Files
pubsub/server/raft_test.go
zooqueen 0d41919cce vet: the test files say what they mean
`go vet ./...` failed, so the gate never reached the tests. Six defects, none
of them subtle once vet names them:

  mqtt_test.go — three tables declare wantErr and their bodies read it, but 18
    rows across them stopped one value short, so the literals did not compile.
    Every one of those cases asserts a specific message, so the value is false.

  stree_test.go — IntersectGSL takes `func([]byte, *T) bool`. One callback in
    LiteralMatchWithOverlappingPWCs returned nothing, unlike the three
    identical callbacks beside it.

  log_test.go, monitor_test.go — %q formats a string; both arguments are ints.

  filestore_test.go, jetstream_consumer_test.go, raft_test.go — unsafe,
    path/filepath and net/url imported and unused.

go vet ./... and go build ./... are both clean now.
2026-08-11 16:58:36 -07:00

5633 lines
176 KiB
Go

// Copyright 2021-2026 The NATS 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 server
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
"math/rand"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go"
)
func TestNRGSimple(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Do several state transitions.
rg.randomMember().(*stateAdder).proposeDelta(22)
rg.randomMember().(*stateAdder).proposeDelta(-11)
rg.randomMember().(*stateAdder).proposeDelta(-10)
// Wait for all members to have the correct state.
rg.waitOnTotal(t, 1)
}
func TestNRGSnapshotAndRestart(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
lsm := rg.waitOnLeader()
require_NotNil(t, lsm)
leader := lsm.(*stateAdder)
var expectedTotal int64
sm := rg.nonLeader().(*stateAdder)
for i := 0; i < 1000; i++ {
delta := rand.Int63n(222)
expectedTotal += delta
leader.proposeDelta(delta)
if i == 250 {
// Let some things catchup.
time.Sleep(50 * time.Millisecond)
// Snapshot leader and stop and snapshot a member.
leader.snapshot(t)
sm.snapshot(t)
sm.stop()
}
}
// Restart.
sm.restart()
// Wait for all members to have the correct state.
rg.waitOnTotal(t, expectedTotal)
}
func TestNRGAppendEntryEncode(t *testing.T) {
ae := &appendEntry{
term: 1,
pindex: 0,
}
// Test leader should be _EMPTY_ or exactly idLen long
ae.leader = "foo_bar_baz"
_, err := ae.encode(nil)
require_Error(t, err, errLeaderLen)
// Empty ok (noLeader)
ae.leader = noLeader // _EMPTY_
_, err = ae.encode(nil)
require_NoError(t, err)
ae.leader = "DEREK123"
_, err = ae.encode(nil)
require_NoError(t, err)
// Buffer reuse
var rawSmall [32]byte
var rawBigger [64]byte
b := rawSmall[:]
ae.encode(b)
if b[0] != 0 {
t.Fatalf("Expected arg buffer to not be used")
}
b = rawBigger[:]
ae.encode(b)
if b[0] == 0 {
t.Fatalf("Expected arg buffer to be used")
}
// Test max number of entries.
for i := 0; i < math.MaxUint16+1; i++ {
ae.entries = append(ae.entries, &Entry{EntryNormal, nil})
}
_, err = ae.encode(b)
require_Error(t, err, errTooManyEntries)
}
func TestNRGAppendEntryDecode(t *testing.T) {
ae := &appendEntry{
leader: "12345678",
term: 1,
pindex: 0,
}
for i := 0; i < math.MaxUint16; i++ {
ae.entries = append(ae.entries, &Entry{EntryNormal, nil})
}
buf, err := ae.encode(nil)
require_NoError(t, err)
// Truncate buffer first.
short := buf[0 : len(buf)-1025]
_, err = decodeAppendEntry(short, nil, _EMPTY_)
require_Error(t, err, errBadAppendEntry)
for i := 0; i < 100; i++ {
b := copyBytes(buf)
// modifying the header (idx < 42) will not result in an error by decodeAppendEntry
bi := 42 + rand.Intn(len(b)-42)
if b[bi] != 0 && bi != 40 {
b[bi] = 0
_, err = decodeAppendEntry(b, nil, _EMPTY_)
require_Error(t, err, errBadAppendEntry)
}
}
}
func TestNRGAppendEntryDecodeTruncatedEntryLength(t *testing.T) {
ae := &appendEntry{
leader: "12345678",
term: 1,
entries: []*Entry{newEntry(EntryNormal, nil)},
}
buf, err := ae.encode(nil)
require_NoError(t, err)
// Truncate the 4 byte length field of the first entry.
// The decoder wants to read 4 bytes, finds only 2 bytes left.
truncated := buf[:appendEntryBaseLen+2]
_, err = decodeAppendEntry(truncated, nil, _EMPTY_)
require_Error(t, err, errBadAppendEntry)
}
func TestNRGRecoverFromFollowingNoLeader(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Find out what term we are on.
term := rg.leader().node().Term()
// Start by pausing all of the nodes. This will stop them from
// processing new entries.
for _, n := range rg {
n.node().PauseApply()
}
// Now drain all of the ApplyQ entries from them, which will stop
// them from automatically trying to follow a previous leader if
// they happened to have received an apply entry from one. Then
// we're going to force them into a state where they are all
// followers but they don't have a leader.
for _, n := range rg {
rn := n.node().(*raft)
rn.ApplyQ().drain()
rn.switchToFollower(noLeader)
}
// Resume the nodes.
for _, n := range rg {
n.node().ResumeApply()
}
// Wait a while. The nodes should notice that they haven't heard
// from a leader lately and will switch to voting. After an
// election we should find a new leader and be on a new term.
rg.waitOnLeader()
require_True(t, rg.leader() != nil)
require_NotEqual(t, rg.leader().node().Term(), term)
}
func TestNRGInlineStepdown(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// When StepDown() completes, we should not be the leader. Before,
// this would not be guaranteed as the stepdown could be processed
// some time later.
n := rg.leader().node().(*raft)
require_NoError(t, n.StepDown())
require_NotEqual(t, n.State(), Leader)
}
func TestNRGObserverMode(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Put all of the followers into observer mode. In this state
// they will not participate in an election but they will continue
// to apply incoming commits.
for _, n := range rg {
if n.node().Leader() {
continue
}
n.node().SetObserver(true)
}
// Propose a change from the leader.
adder := rg.leader().(*stateAdder)
adder.proposeDelta(1)
adder.proposeDelta(2)
adder.proposeDelta(3)
// Wait for the followers to apply it.
rg.waitOnTotal(t, 6)
// Confirm the followers are still just observers and weren't
// reset out of that state for some reason.
for _, n := range rg {
if n.node().Leader() {
continue
}
require_True(t, n.node().IsObserver())
}
}
func TestNRGAEFromOldLeader(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Listen out for catchup requests.
ch := make(chan *nats.Msg, 16)
_, err := nc.ChanSubscribe(fmt.Sprintf(raftCatchupReply, ">"), ch)
require_NoError(t, err)
// Start next term so that we can reuse term 1 in the next step.
leader := rg.leader().node().(*raft)
leader.StepDown()
time.Sleep(time.Millisecond * 100)
rg.waitOnLeader()
require_Equal(t, leader.Term(), 2)
leader = rg.leader().node().(*raft)
// Send an append entry with an outdated term. Beforehand, doing
// so would have caused a WAL reset and then would have triggered
// a Raft-level catchup.
ae := &appendEntry{
term: 1,
pindex: 0,
leader: leader.id,
reply: nc.NewRespInbox(),
}
payload, err := ae.encode(nil)
require_NoError(t, err)
resp, err := nc.Request(leader.asubj, payload, time.Second)
require_NoError(t, err)
// Wait for the response, the server should have rejected it.
ar := decodeAppendEntryResponse(resp.Data)
require_NotNil(t, ar)
require_Equal(t, ar.success, false)
// No catchup should happen at this point because no reset should
// have happened.
require_NoChanRead(t, ch, time.Second*2)
}
// TestNRGSimpleElection tests that a simple election succeeds. It is
// simple because the group hasn't processed any entries and hasn't
// suffered any interruptions of any kind, therefore there should be
// no way that the conditions for granting the votes can fail.
func TestNRGSimpleElection(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 9)
defer c.shutdown()
c.waitOnLeader()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createRaftGroup("TEST", 9, newStateAdder)
rg.waitOnLeader()
voteReqs := make(chan *nats.Msg, 1)
voteResps := make(chan *nats.Msg, len(rg)-1)
// Keep a record of the term when we started.
leader := rg.leader().node().(*raft)
startTerm := leader.term
// Subscribe to the vote request subject, this should be the
// same across all nodes in the group.
_, err := nc.ChanSubscribe(leader.vsubj, voteReqs)
require_NoError(t, err)
// Subscribe to all of the vote response inboxes for all nodes
// in the Raft group, as they can differ.
for _, n := range rg {
rn := n.node().(*raft)
_, err = nc.ChanSubscribe(rn.vreply, voteResps)
require_NoError(t, err)
}
// Step down, this will start a new voting session.
require_NoError(t, rg.leader().node().StepDown())
// Wait for a vote request to come in.
msg := require_ChanRead(t, voteReqs, time.Second)
vr := decodeVoteRequest(msg.Data, msg.Reply)
require_True(t, vr != nil)
require_NotEqual(t, vr.candidate, _EMPTY_)
// The leader should have bumped their term in order to start
// an election.
require_Equal(t, vr.term, startTerm+1)
require_Equal(t, vr.lastTerm, startTerm)
// Wait for all of the vote responses to come in. There should
// be as many vote responses as there are followers.
for i := 0; i < len(rg)-1; i++ {
msg := require_ChanRead(t, voteResps, time.Second)
re := decodeVoteResponse(msg.Data)
require_True(t, re != nil)
// Ignore old vote responses that could be in-flight.
if re.term < vr.term {
i--
continue
}
// The vote should have been granted.
require_Equal(t, re.granted, true)
// The node granted the vote, therefore the term in the vote
// response should have advanced as well.
require_Equal(t, re.term, vr.term)
require_Equal(t, re.term, startTerm+1)
}
// Everyone in the group should have voted for our candidate
// and arrived at the term from the vote request.
rg.lockAll()
defer rg.unlockAll()
for _, n := range rg {
rn := n.node().(*raft)
require_Equal(t, rn.term, vr.term)
require_Equal(t, rn.term, startTerm+1)
require_Equal(t, rn.vote, vr.candidate)
}
}
// TestNRGLeaderTransfer verifies that a Raft group correctly transfers
// leadership to a chosen preferred node when the current leader steps down.
func TestNRGLeaderTransfer(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createMemRaftGroup("Test", 3, newStateAdder)
rg.waitOnLeader()
leader := rg.leader()
sub, err := nc.SubscribeSync(leader.node().(*raft).asubj)
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
preferredID := rg.nonLeader().node().ID()
leader.node().StepDown(preferredID)
newLeader := rg.waitOnLeader()
require_Equal(t, newLeader.node().ID(), preferredID)
// Expect to see a EntryLeader message
checkFor(t, time.Second, 0, func() (err error) {
msg, err := sub.NextMsg(time.Second)
if err != nil {
return err
}
ae, err := decodeAppendEntry(msg.Data, nil, msg.Reply)
if err != nil {
return err
}
if len(ae.entries) == 1 {
e := ae.entries[0]
if e.Type == EntryLeaderTransfer && string(e.Data) == preferredID {
return nil
}
}
return fmt.Errorf("Expect EntryLeaderTransfer")
})
}
func TestNRGSwitchStateClearsQueues(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
s := c.servers[0] // RunBasicJetStreamServer not available
n := &raft{
prop: newIPQueue[*proposedEntry](s, "prop"),
resp: newIPQueue[*appendEntryResponse](s, "resp"),
leadc: make(chan bool, 1), // for switchState
sd: t.TempDir(),
}
n.state.Store(int32(Leader))
require_Equal(t, n.prop.len(), 0)
require_Equal(t, n.resp.len(), 0)
n.prop.push(&proposedEntry{&Entry{}, _EMPTY_})
n.resp.push(&appendEntryResponse{})
require_Equal(t, n.prop.len(), 1)
require_Equal(t, n.resp.len(), 1)
n.switchState(Follower)
require_Equal(t, n.prop.len(), 0)
require_Equal(t, n.resp.len(), 0)
}
func TestNRGStepDownOnSameTermDoesntClearVote(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
c.waitOnLeader()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
lsm := rg.leader().(*stateAdder)
leader := lsm.node().(*raft)
follower := rg.nonLeader().node().(*raft)
// Make sure we handle the leader change notification from above.
require_ChanRead(t, lsm.lch, time.Second)
// Subscribe to the append entry subject.
sub, err := nc.SubscribeSync(leader.asubj)
require_NoError(t, err)
// Get the first append entry that we receive.
msg, err := sub.NextMsg(time.Second)
require_NoError(t, err)
require_NoError(t, sub.Unsubscribe())
// We're going to modify the append entry that we received so that
// we can send it again with modifications.
ae, err := decodeAppendEntry(msg.Data, nil, msg.Reply)
require_NoError(t, err)
// First of all we're going to try sending an append entry that
// has an old term and a fake leader.
msg.Reply = follower.areply
ae.leader = follower.id
ae.term = leader.term - 1
msg.Data, err = ae.encode(msg.Data[:0])
require_NoError(t, err)
require_NoError(t, nc.PublishMsg(msg))
// Because the term was old, the fake leader shouldn't matter as
// the current leader should ignore it.
require_NoChanRead(t, lsm.lch, time.Second)
// Now we're going to send it on the same term that the current leader
// is on. What we expect to happen is that the leader will step down
// but it *shouldn't* clear the vote.
ae.term = leader.term
msg.Data, err = ae.encode(msg.Data[:0])
require_NoError(t, err)
require_NoError(t, nc.PublishMsg(msg))
// Wait for the leader transition and ensure that the vote wasn't
// cleared.
require_ChanRead(t, lsm.lch, time.Second)
require_NotEqual(t, leader.vote, noVote)
}
func TestNRGUnsuccessfulVoteRequestDoesntResetElectionTimer(t *testing.T) {
// This test relies on nodes not hitting their election timer too often,
// otherwise the step later where we capture the election time before and
// after the failed vote request will flake.
origMinTimeout, origMaxTimeout, origHBInterval := minElectionTimeout, maxElectionTimeout, hbInterval
minElectionTimeout, maxElectionTimeout, hbInterval = time.Second*5, time.Second*10, time.Second*10
defer func() {
minElectionTimeout, maxElectionTimeout, hbInterval = origMinTimeout, origMaxTimeout, origHBInterval
}()
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
c.waitOnLeader()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
// Because the election timer is quite high, we want to kick a node into
// campaigning before it naturally needs to, otherwise the test takes a
// long time just to pick a leader.
for _, n := range rg {
n.node().Campaign()
break
}
rg.waitOnLeader()
rgLeader := rg.leader()
leader := rgLeader.node().(*raft)
follower := rg.nonLeader().node().(*raft)
// Send one message to ensure heartbeats are not sent during the remainder of this test.
rgLeader.(*stateAdder).proposeDelta(1)
rg.waitOnTotal(t, 1)
// Set up a new inbox for the vote responses to go to.
vsubj, vreply := leader.vsubj, nc.NewInbox()
ch := make(chan *nats.Msg, 3)
_, err := nc.ChanSubscribe(vreply, ch)
require_NoError(t, err)
// Keep a track of the last time the election timer was reset before this.
// Also build up a vote request that's obviously behind so that the other
// nodes should not do anything with it. All locks are taken at the same
// time so that it guarantees that both the leader and the follower aren't
// operating at the time we take the etlr snapshots.
rg.lockAll()
leader.resetElect(maxElectionTimeout)
follower.resetElect(maxElectionTimeout)
leaderOriginal := leader.etlr
followerOriginal := follower.etlr
vr := &voteRequest{
term: follower.term - 1,
lastTerm: follower.term - 1,
lastIndex: 0,
candidate: follower.id,
}
rg.unlockAll()
// Now send a vote request that's obviously behind.
require_NoError(t, nc.PublishMsg(&nats.Msg{
Subject: vsubj,
Reply: vreply,
Data: vr.encode(),
}))
// Wait for everyone to respond.
require_ChanRead(t, ch, time.Second)
require_ChanRead(t, ch, time.Second)
require_ChanRead(t, ch, time.Second)
// Neither the leader nor our chosen follower should have updated their
// election timer as a result of this.
rg.lockAll()
leaderEqual := leaderOriginal.Equal(leader.etlr)
followerEqual := followerOriginal.Equal(follower.etlr)
rg.unlockAll()
require_True(t, leaderEqual)
require_True(t, followerEqual)
}
func TestNRGUnsuccessfulVoteRequestCampaignEarly(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats0 := "S1Nunr6R" // "nats-0"
n.etlr = time.Time{}
// Simple case: we are follower and vote for a candidate.
require_NoError(t, n.processVoteRequest(&voteRequest{term: 1, lastTerm: 0, lastIndex: 0, candidate: nats0}))
require_Equal(t, n.term, 1)
require_Equal(t, n.vote, nats0)
require_NotEqual(t, n.etlr, time.Time{}) // Resets election timer as it voted.
n.etlr = time.Time{}
// We are follower and deny vote for outdated candidate.
n.pterm, n.pindex = 1, 100
require_NoError(t, n.processVoteRequest(&voteRequest{term: 2, lastTerm: 1, lastIndex: 2, candidate: nats0}))
require_Equal(t, n.term, 2)
require_Equal(t, n.vote, noVote)
require_NotEqual(t, n.etlr, time.Time{}) // Resets election timer as it starts campaigning.
n.etlr = time.Time{}
// Switch to candidate.
n.pterm, n.pindex = 2, 200
n.switchToCandidate()
require_Equal(t, n.term, 3)
require_Equal(t, n.State(), Candidate)
require_NotEqual(t, n.etlr, time.Time{}) // Resets election timer as part of switching state.
n.etlr = time.Time{}
// We are candidate and deny vote for outdated candidate. But they were on a more recent term, restart campaign.
require_NoError(t, n.processVoteRequest(&voteRequest{term: 4, lastTerm: 1, lastIndex: 2, candidate: nats0}))
require_Equal(t, n.term, 4)
require_Equal(t, n.vote, noVote)
require_NotEqual(t, n.etlr, time.Time{}) // Resets election timer as it restarts campaigning.
n.etlr = time.Time{}
// Switch to candidate.
n.pterm, n.pindex = 4, 400
n.switchToCandidate()
require_Equal(t, n.term, 5)
require_Equal(t, n.State(), Candidate)
require_NotEqual(t, n.etlr, time.Time{}) // Resets election timer as part of switching state.
n.etlr = time.Time{}
// We are candidate and deny vote for outdated candidate. Don't start campaigning early.
require_NoError(t, n.processVoteRequest(&voteRequest{term: 5, lastTerm: 1, lastIndex: 2, candidate: nats0}))
require_Equal(t, n.term, 5)
require_Equal(t, n.vote, noVote)
// Election timer must NOT be updated as that would mean another candidate that we don't vote
// for can short-circuit us by making us restart elections, denying us the ability to become leader.
require_Equal(t, n.etlr, time.Time{})
}
func TestNRGInvalidTAVDoesntPanic(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
c.waitOnLeader()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
leader := rg.waitOnLeader()
require_NotNil(t, leader)
// Mangle the TAV file to a short length (less than uint64).
tav := filepath.Join(leader.node().(*raft).sd, termVoteFile)
require_NoError(t, os.WriteFile(tav, []byte{1, 2, 3, 4}, 0644))
// Restart the node.
leader.stop()
leader.restart()
// Before the fix, a crash would have happened before this point.
c.waitOnAllCurrent()
}
func TestNRGAssumeHighTermAfterCandidateIsolation(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
c.waitOnLeader()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Bump the term up on one of the follower nodes by a considerable
// amount and force it into the candidate state. This is what happens
// after a period of time in isolation.
follower := rg.nonLeader().node().(*raft)
follower.Lock()
follower.term += 100
follower.switchState(Candidate)
follower.Unlock()
follower.requestVote()
time.Sleep(time.Millisecond * 100)
// The candidate will shortly send a vote request. When that happens,
// the rest of the nodes in the cluster should move up to that term,
// even though they will not grant the vote.
nterm := follower.term
for _, n := range rg {
require_Equal(t, n.node().Term(), nterm)
}
// Have the leader send out a proposal, which will force the candidate
// back into follower state.
rg.waitOnLeader()
rg.leader().(*stateAdder).proposeDelta(1)
rg.waitOnTotal(t, 1)
// The candidate should have switched to a follower on a term equal to
// or newer than the candidate had.
for _, n := range rg {
require_NotEqual(t, n.node().State(), Candidate)
require_True(t, n.node().Term() >= nterm)
}
}
// Test to make sure this does not cause us to truncate our wal or enter catchup state.
func TestNRGHeartbeatOnLeaderChange(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
for i := 0; i < 10; i++ {
// Restart the leader.
leader := rg.leader().(*stateAdder)
leader.proposeDelta(22)
leader.proposeDelta(-11)
leader.proposeDelta(-10)
// Must observe forward progress, so each iteration will check +1 total.
rg.waitOnTotal(t, int64(i+1))
leader.stop()
leader.restart()
rg.waitOnLeader()
}
}
func TestNRGElectionTimerAfterObserver(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
for _, n := range rg {
n.node().SetObserver(true)
}
time.Sleep(maxElectionTimeout)
before := time.Now()
for _, n := range rg {
n.node().SetObserver(false)
}
time.Sleep(maxCampaignTimeout)
for _, n := range rg {
rn := n.node().(*raft)
rn.RLock()
etlr := rn.etlr
rn.RUnlock()
require_True(t, etlr.After(before))
}
}
func TestNRGSystemClientCleanupFromAccount(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
s := c.randomServer()
sacc := s.SystemAccount()
numClients := func() int {
sacc.mu.RLock()
defer sacc.mu.RUnlock()
return len(sacc.clients)
}
start := numClients()
var all []smGroup
for i := 0; i < 5; i++ {
rgName := fmt.Sprintf("TEST-%d", i)
rg := c.createRaftGroup(rgName, 3, newStateAdder)
all = append(all, rg)
rg.waitOnLeader()
}
for _, rg := range all {
for _, sm := range rg {
sm.node().Stop()
}
for _, sm := range rg {
sm.node().WaitForStop()
}
}
finish := numClients()
require_Equal(t, start, finish)
}
func TestNRGCandidateDoesntRevertTermAfterOldAE(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Bump the term up a few times.
for i := 0; i < 3; i++ {
rg.leader().node().StepDown()
time.Sleep(time.Millisecond * 50) // Needed because stepdowns not synchronous
rg.waitOnLeader()
}
leader := rg.leader().node().(*raft)
follower := rg.nonLeader().node().(*raft)
// Sanity check that we are where we expect to be.
require_Equal(t, leader.term, 4)
require_Equal(t, follower.term, 4)
// At this point the active term is 4 and pterm is 4, force the
// term up to 9. This won't bump the pterm.
rg.lockAll()
for _, n := range rg {
n.node().(*raft).term += 5
}
rg.unlockAll()
// Build an AE that has a term newer than the pterm but older than
// the term. Give it to the follower in candidate state.
ae := newAppendEntry(leader.id, 6, leader.commit, leader.pterm, leader.pindex, nil)
follower.switchToCandidate()
follower.processAppendEntry(ae, nil)
// The candidate must not have reverted back to term 6.
require_NotEqual(t, follower.term, 6)
}
func TestNRGTermDoesntRollBackToPtermOnCatchup(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Propose some entries so that we have entries in the log that have pterm 1.
lsm := rg.leader().(*stateAdder)
for i := 0; i < 5; i++ {
lsm.proposeDelta(1)
rg.waitOnTotal(t, int64(i)+1)
}
// Check that everyone is where they are supposed to be.
rg.lockAll()
for _, n := range rg {
rn := n.node().(*raft)
require_Equal(t, rn.term, 1)
require_Equal(t, rn.pterm, 1)
require_Equal(t, rn.pindex, 6)
}
rg.unlockAll()
// Force a stepdown so that we move up to term 2.
rg.leader().node().(*raft).switchToFollower(noLeader)
rg.waitOnLeader()
leader := rg.leader().node().(*raft)
// Now make sure everyone has moved up to term 2. Additionally we're
// going to prune back the follower logs to term 1 as this is what will
// create the right conditions for the catchup.
rg.lockAll()
for _, n := range rg {
rn := n.node().(*raft)
require_Equal(t, rn.term, 2)
if !rn.Leader() {
rn.truncateWAL(1, 6)
require_Equal(t, rn.term, 2) // rn.term must stay the same
require_Equal(t, rn.pterm, 1)
require_Equal(t, rn.pindex, 6)
}
}
// This will make followers run a catchup.
ae := newAppendEntry(leader.id, leader.term, leader.commit, leader.pterm, leader.pindex, nil)
rg.unlockAll()
arInbox := nc.NewRespInbox()
arCh := make(chan *nats.Msg, 2)
_, err := nc.ChanSubscribe(arInbox, arCh)
require_NoError(t, err)
// Ensure the subscription is known by the server we're connected to,
// but also by the other servers in the cluster.
require_NoError(t, nc.Flush())
time.Sleep(100 * time.Millisecond)
// In order to trip this condition, we need to send an append entry that
// will trick the followers into running a catchup. In the process they
// were setting the term back to pterm which is incorrect.
b, err := ae.encode(nil)
require_NoError(t, err)
require_NoError(t, nc.PublishMsg(&nats.Msg{
Subject: fmt.Sprintf(raftAppendSubj, "TEST"),
Reply: arInbox,
Data: b,
}))
// Wait for both followers to respond to the append entry and then verify
// that none of the nodes should have reverted back to term 1.
require_ChanRead(t, arCh, time.Second*5) // First follower
require_ChanRead(t, arCh, time.Second*5) // Second follower
for _, n := range rg {
require_NotEqual(t, n.node().Term(), 1)
}
}
func TestNRGNoResetOnAppendEntryResponse(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
c.waitOnAllCurrent()
leader := rg.leader().node().(*raft)
follower := rg.nonLeader().node().(*raft)
lsm := rg.leader().(*stateAdder)
isLeader := require_ChanRead(t, lsm.lch, 5*time.Second)
require_True(t, isLeader)
// Subscribe for append entries that aren't heartbeats and respond to
// each of them as though it's a non-success and with a higher term.
// The higher term in this case is what would cause the leader previously
// to reset the entire log which it shouldn't do.
_, err := nc.Subscribe(fmt.Sprintf(raftAppendSubj, "TEST"), func(msg *nats.Msg) {
if ae, err := decodeAppendEntry(msg.Data, nil, msg.Reply); err == nil && len(ae.entries) > 0 {
ar := newAppendEntryResponse(ae.term+1, ae.commit, follower.id, false)
require_NoError(t, msg.Respond(ar.encode(nil)))
}
})
require_NoError(t, err)
c.waitOnAllCurrent()
// Temporarily prevent followers to respond
locked := rg.lockFollowers()
// Generate an append entry that the subscriber above can respond to.
lsm.proposeDelta(5)
// Expect the leader to step down
checkFor(t, 10*time.Second, 0, func() error {
isLeader = require_ChanRead(t, lsm.lch, 5*time.Second)
if isLeader {
return fmt.Errorf("Node is still leader!")
}
return nil
})
// Unlock the followers
for _, l := range locked {
l.node().(*raft).Unlock()
}
// The was-leader should now have stepped down, make sure that it
// didn't blow away its log in the process.
rg.lockAll()
defer rg.unlockAll()
require_Equal(t, leader.State(), Follower)
require_NotEqual(t, leader.pterm, 0)
require_NotEqual(t, leader.pindex, 0)
}
func TestNRGCandidateDontStepdownDueToLeaderOfPreviousTerm(t *testing.T) {
// This test relies on nodes not hitting their election timer too often.
origMinTimeout, origMaxTimeout, origHBInterval := minElectionTimeout, maxElectionTimeout, hbInterval
minElectionTimeout, maxElectionTimeout, hbInterval = time.Second*5, time.Second*10, time.Second
defer func() {
minElectionTimeout, maxElectionTimeout, hbInterval = origMinTimeout, origMaxTimeout, origHBInterval
}()
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
c.waitOnLeader()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
var (
candidatePterm uint64 = 50
candidatePindex uint64 = 70
candidateTerm uint64 = 100
)
// Create a candidate that has received entries while they were a follower in a previous term
candidate := rg.nonLeader().node().(*raft)
candidate.Lock()
candidate.switchState(Candidate)
candidate.pterm = candidatePterm
candidate.pindex = candidatePindex
candidate.term = candidateTerm
candidate.Unlock()
// Leader term is behind candidate
leader := rg.leader().node().(*raft)
leader.Lock()
leader.term = candidatePterm
leader.pterm = candidatePterm
leader.pindex = candidatePindex
leader.Unlock()
// Subscribe to the append entry subject.
sub, err := nc.SubscribeSync(leader.asubj)
require_NoError(t, err)
// Get the first append entry that we receive, should be heartbeat from leader of prev term
msg, err := sub.NextMsg(5 * time.Second)
require_NoError(t, err)
// Stop nodes from progressing so we can check state
rg.lockAll()
defer rg.unlockAll()
// Decode the append entry
ae, err := decodeAppendEntry(msg.Data, nil, msg.Reply)
require_NoError(t, err)
// Check that the append entry is from the leader
require_Equal(t, ae.leader, leader.id)
// Check that it came from the leader before it updated its term with the response from the candidate
require_Equal(t, ae.term, candidatePterm)
// Check that the candidate hasn't stepped down
require_Equal(t, candidate.State(), Candidate)
// Check that the candidate's term is still ahead of the leader's term
require_True(t, candidate.term > ae.term)
}
func TestNRGRemoveLeaderPeerDeadlockBug(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
n := rg.leader().node().(*raft)
leader := n.ID()
// Propose to remove the leader as a peer. Will lead to a deadlock with bug.
require_NoError(t, n.ProposeRemovePeer(leader))
rg.waitOnLeader()
checkFor(t, 10*time.Second, 200*time.Millisecond, func() error {
nl := n.GroupLeader()
if nl != leader {
return nil
}
return errors.New("Leader has not moved")
})
}
func TestNRGWALEntryWithoutQuorumMustTruncate(t *testing.T) {
tests := []struct {
title string
modify func(rg smGroup)
}{
{
// state equals, only need to remove the entry
title: "equal",
modify: func(rg smGroup) {},
},
{
// state diverged, need to replace the entry
title: "diverged",
modify: func(rg smGroup) {
rg.leader().(*stateAdder).proposeDelta(11)
},
},
}
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
var err error
var scratch [1024]byte
// Simulate leader storing an AppendEntry in WAL but being hard killed before it can propose to its peers.
n := rg.leader().node().(*raft)
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
n.Lock()
ae := n.buildAppendEntry(entries)
ae.buf, err = ae.encode(scratch[:])
require_NoError(t, err)
err = n.storeToWAL(ae)
n.Unlock()
require_NoError(t, err)
// Stop the leader so it moves to another one.
n.shutdown()
// Wait for another leader to be picked
rg.waitOnLeader()
// Make a modification, specific to this test.
test.modify(rg)
// Restart the previous leader that contains the stored AppendEntry without quorum.
for _, a := range rg {
if a.node().ID() == n.ID() {
sa := a.(*stateAdder)
sa.restart()
break
}
}
// The previous leader's WAL should truncate to remove the AppendEntry only it has.
// Eventually all WALs for all peers must match.
checkFor(t, 5*time.Second, 200*time.Millisecond, func() error {
var expected []*appendEntry
for _, a := range rg {
an := a.node().(*raft)
var state StreamState
an.wal.FastState(&state)
if len(expected) > 0 && int(state.LastSeq-state.FirstSeq+1) != len(expected) {
return fmt.Errorf("WAL is different: too many entries")
}
// Loop over all entries in the WAL, checking if the contents for all RAFT nodes are equal.
for index := state.FirstSeq; index <= state.LastSeq; index++ {
ae, err := an.loadEntry(index)
if err != nil {
return err
}
ae.buf = nil // ... as we'll deeply check everything else in the AE.
ae.lterm = 0 // ... as lterm can differ if one node caught up another.
seq := int(index)
if len(expected) < seq {
expected = append(expected, ae)
} else if !reflect.DeepEqual(expected[seq-1], ae) {
return fmt.Errorf("WAL is different: stored AEs differ")
}
}
}
return nil
})
})
}
}
func TestNRGTermNoDecreaseAfterWALReset(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
l := rg.leader().node().(*raft)
l.Lock()
l.term = 20
l.Unlock()
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
l.Lock()
ae := l.buildAppendEntry(entries)
l.Unlock()
for _, f := range rg {
if f.node().ID() != l.ID() {
fn := f.node().(*raft)
fn.processAppendEntry(ae, fn.aesub)
require_Equal(t, fn.term, 20) // Follower's term gets upped as expected.
}
}
// Lower the term, simulating the followers receiving a message from an old term/leader.
ae.term = 3
for _, f := range rg {
if f.node().ID() != l.ID() {
fn := f.node().(*raft)
fn.processAppendEntry(ae, fn.aesub)
require_Equal(t, fn.term, 20) // Follower should reject and the term stays the same.
fn.Lock()
fn.resetWAL()
fn.Unlock()
fn.processAppendEntry(ae, fn.aesub)
require_Equal(t, fn.term, 20) // Follower should reject again, even after reset, term stays the same.
}
}
}
func TestNRGPendingAppendEntryCacheInvalidation(t *testing.T) {
for _, test := range []struct {
title string
entries int
}{
{title: "empty", entries: 1},
{title: "at limit", entries: paeDropThreshold},
{title: "full", entries: paeDropThreshold + 1},
} {
t.Run(test.title, func(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
l := rg.leader()
l.(*stateAdder).proposeDelta(1)
rg.waitOnTotal(t, 1)
// Fill up the cache with N entries.
// The contents don't matter as they should never be applied.
rg.lockAll()
for _, s := range rg {
n := s.node().(*raft)
for i := 0; i < test.entries; i++ {
n.pae[n.pindex+uint64(1+i)] = newAppendEntry("", 0, 0, 0, 0, nil)
}
}
rg.unlockAll()
l.(*stateAdder).proposeDelta(1)
rg.waitOnTotal(t, 2)
})
}
}
func TestNRGTruncateWALClearsPendingAppendEntryCache(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Store three entries through the normal path so both the WAL and n.pae
// are populated by cachePendingEntry.
ae1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
ae2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
ae3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 2, entries: entries})
n.processAppendEntry(ae1, n.aesub)
n.processAppendEntry(ae2, n.aesub)
n.processAppendEntry(ae3, n.aesub)
n.Lock()
defer n.Unlock()
// Sanity check: WAL and cache both have all three entries.
require_Equal(t, n.pindex, 3)
require_Equal(t, n.wal.State().Msgs, 3)
require_NotNil(t, n.pae[1])
require_NotNil(t, n.pae[2])
require_NotNil(t, n.pae[3])
// Truncate WAL back to index 1, dropping entries 2 and 3 from the log.
n.truncateWAL(1, 1)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.wal.State().Msgs, 1)
// The cache must not retain entries for indices the WAL no longer holds,
// otherwise applyCommit could later apply stale entries straight from
// the cache (bypassing the WAL load path).
require_Len(t, len(n.pae), 1)
require_NotNil(t, n.pae[1])
}
func TestNRGResetWALClearsPendingAppendEntryCache(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
ae1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
ae2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
ae3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 2, entries: entries})
n.processAppendEntry(ae1, n.aesub)
n.processAppendEntry(ae2, n.aesub)
n.processAppendEntry(ae3, n.aesub)
n.Lock()
defer n.Unlock()
require_Equal(t, n.pindex, 3)
require_Equal(t, len(n.pae), 3)
n.resetWAL()
require_Equal(t, n.pindex, 0)
require_Equal(t, n.wal.State().Msgs, 0)
// All cached pending append entries should be gone after a full reset.
require_Equal(t, len(n.pae), 0)
}
func TestNRGInstallSnapshotClearsPendingAppendEntryCache(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Three appendEntries arrive with commit lagging at 0, so applyCommit
// never runs and all three remain cached in n.pae.
ae1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
ae2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
ae3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 2, entries: entries})
n.processAppendEntry(ae1, n.aesub)
n.processAppendEntry(ae2, n.aesub)
n.processAppendEntry(ae3, n.aesub)
n.Lock()
defer n.Unlock()
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 0)
require_Equal(t, len(n.pae), 3)
// Install a snapshot covering the entire log. The WAL gets compacted
// past index 3, but cached entries 1-3 would otherwise stay in n.pae
// since applyCommit never ran for them and cachePendingEntry only
// overwrites at the current n.pindex (which won't revisit 1-3).
snap := &snapshot{
lastTerm: 1,
lastIndex: 3,
peerstate: encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt}),
}
require_NoError(t, n.installSnapshot(snap))
require_Equal(t, n.wal.State().Msgs, 0)
require_Equal(t, len(n.pae), 0)
}
func TestNRGCatchupFromSnapshotClearsPendingAppendEntryCache(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
snapshotEntries := []*Entry{
newEntry(EntrySnapshot, nil),
newEntry(EntryPeerState, encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt})),
}
nats0 := "S1Nunr6R" // "nats-0"
// Receive two appendEntries normally; commit lags so both stay cached.
ae1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
ae2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
n.processAppendEntry(ae1, n.aesub)
n.processAppendEntry(ae2, n.aesub)
require_Equal(t, n.pindex, 2)
require_Equal(t, len(n.pae), 2)
// Leader is way ahead; this triggers catchup.
aeTriggerCatchup := encode(t, &appendEntry{leader: nats0, term: 1, commit: 100, pterm: 1, pindex: 100, entries: nil})
n.processAppendEntry(aeTriggerCatchup, n.aesub)
require_True(t, n.catchup != nil)
// Leader sends a snapshot at the catchup point. n.pindex/n.commit jump
// to 100 without applyCommit running for indices 1-2, so the cached
// entries from before catchup would otherwise leak.
aeCatchupSnapshot := encode(t, &appendEntry{leader: nats0, term: 1, commit: 100, pterm: 1, pindex: 100, entries: snapshotEntries})
n.processAppendEntry(aeCatchupSnapshot, n.catchup.sub)
n.Lock()
defer n.Unlock()
require_Equal(t, n.pindex, 100)
require_Equal(t, n.commit, 100)
require_Equal(t, len(n.pae), 0)
}
func TestNRGCatchupDoesNotTruncateUncommittedEntriesWithQuorum(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
nats1 := "yrzKKRBu" // "nats-1"
// Timeline, for first leader
aeInitial := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
aeUncommitted := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeNoQuorum := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 2, entries: entries})
// Timeline, after leader change
aeMissed := encode(t, &appendEntry{leader: nats1, term: 2, commit: 1, pterm: 1, pindex: 2, entries: entries})
aeCatchupTrigger := encode(t, &appendEntry{leader: nats1, term: 2, commit: 1, pterm: 2, pindex: 3, entries: entries})
aeHeartbeat2 := encode(t, &appendEntry{leader: nats1, term: 2, commit: 2, pterm: 2, pindex: 4, entries: nil})
aeHeartbeat3 := encode(t, &appendEntry{leader: nats1, term: 2, commit: 4, pterm: 2, pindex: 4, entries: nil})
// Initial case is simple, just store the entry.
n.processAppendEntry(aeInitial, n.aesub)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err := n.loadEntry(1)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Heartbeat, makes sure commit moves up.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 1)
// We get one entry that has quorum (but we don't know that yet), so it stays uncommitted for a bit.
n.processAppendEntry(aeUncommitted, n.aesub)
require_Equal(t, n.wal.State().Msgs, 2)
entry, err = n.loadEntry(2)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// We get one entry that has NO quorum (but we don't know that yet).
n.processAppendEntry(aeNoQuorum, n.aesub)
require_Equal(t, n.wal.State().Msgs, 3)
entry, err = n.loadEntry(3)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// We've just had a leader election, and we missed one message from the previous leader.
// We should truncate the last message.
n.processAppendEntry(aeCatchupTrigger, n.aesub)
require_Equal(t, n.wal.State().Msgs, 2)
require_True(t, n.catchup == nil)
// We get a heartbeat that prompts us to catchup.
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_Equal(t, n.wal.State().Msgs, 2)
require_Equal(t, n.commit, 1) // Commit should not change, as we missed an item.
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 1) // n.pterm
require_Equal(t, n.catchup.pindex, 2) // n.pindex
// We now notice the leader indicated a different entry at the (no quorum) index, should save that.
n.processAppendEntry(aeMissed, n.catchup.sub)
require_Equal(t, n.wal.State().Msgs, 3)
require_True(t, n.catchup != nil)
// We now get the entry that initially triggered us to catchup, it should be added.
n.processAppendEntry(aeCatchupTrigger, n.catchup.sub)
require_Equal(t, n.wal.State().Msgs, 4)
require_True(t, n.catchup != nil)
entry, err = n.loadEntry(4)
require_NoError(t, err)
require_Equal(t, entry.leader, nats1)
// Heartbeat, makes sure we commit (and reset catchup, as we're now up-to-date).
n.processAppendEntry(aeHeartbeat3, n.aesub)
require_Equal(t, n.commit, 4)
require_True(t, n.catchup == nil)
}
func TestNRGCatchupCanTruncateMultipleEntriesWithoutQuorum(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
nats1 := "yrzKKRBu" // "nats-1"
// Timeline, for first leader
aeInitial := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
aeNoQuorum1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeNoQuorum2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 2, entries: entries})
// Timeline, after leader change
aeMissed1 := encode(t, &appendEntry{leader: nats1, term: 2, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeMissed2 := encode(t, &appendEntry{leader: nats1, term: 2, commit: 1, pterm: 2, pindex: 2, entries: entries})
aeCatchupTrigger := encode(t, &appendEntry{leader: nats1, term: 2, commit: 1, pterm: 2, pindex: 3, entries: entries})
aeHeartbeat2 := encode(t, &appendEntry{leader: nats1, term: 2, commit: 2, pterm: 2, pindex: 4, entries: nil})
aeHeartbeat3 := encode(t, &appendEntry{leader: nats1, term: 2, commit: 4, pterm: 2, pindex: 4, entries: nil})
// Initial case is simple, just store the entry.
n.processAppendEntry(aeInitial, n.aesub)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err := n.loadEntry(1)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Heartbeat, makes sure commit moves up.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 1)
// We get one entry that has NO quorum (but we don't know that yet).
n.processAppendEntry(aeNoQuorum1, n.aesub)
require_Equal(t, n.wal.State().Msgs, 2)
entry, err = n.loadEntry(2)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// We get another entry that has NO quorum (but we don't know that yet).
n.processAppendEntry(aeNoQuorum2, n.aesub)
require_Equal(t, n.wal.State().Msgs, 3)
entry, err = n.loadEntry(3)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// We've just had a leader election, and we missed messages from the previous leader.
// We should truncate the last message.
n.processAppendEntry(aeCatchupTrigger, n.aesub)
require_Equal(t, n.wal.State().Msgs, 2)
require_True(t, n.catchup == nil)
// We get a heartbeat that prompts us to catchup.
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_Equal(t, n.wal.State().Msgs, 2)
require_Equal(t, n.commit, 1) // Commit should not change, as we missed an item.
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 1) // n.pterm
require_Equal(t, n.catchup.pindex, 2) // n.pindex
// We now notice the leader indicated a different entry at the (no quorum) index. We should truncate again.
n.processAppendEntry(aeMissed2, n.catchup.sub)
require_Equal(t, n.wal.State().Msgs, 1)
require_True(t, n.catchup == nil)
// We get a heartbeat that prompts us to catchup.
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_Equal(t, n.wal.State().Msgs, 1)
require_Equal(t, n.commit, 1) // Commit should not change, as we missed an item.
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 1) // n.pterm
require_Equal(t, n.catchup.pindex, 1) // n.pindex
// We now get caught up with the missed messages.
n.processAppendEntry(aeMissed1, n.catchup.sub)
require_Equal(t, n.wal.State().Msgs, 2)
require_True(t, n.catchup != nil)
n.processAppendEntry(aeMissed2, n.catchup.sub)
require_Equal(t, n.wal.State().Msgs, 3)
require_True(t, n.catchup != nil)
// We now get the entry that initially triggered us to catchup, it should be added.
n.processAppendEntry(aeCatchupTrigger, n.catchup.sub)
require_Equal(t, n.wal.State().Msgs, 4)
require_True(t, n.catchup != nil)
entry, err = n.loadEntry(4)
require_NoError(t, err)
require_Equal(t, entry.leader, nats1)
// Heartbeat, makes sure we commit (and reset catchup, as we're now up-to-date).
n.processAppendEntry(aeHeartbeat3, n.aesub)
require_Equal(t, n.commit, 4)
require_True(t, n.catchup == nil)
}
func TestNRGCatchupDoesNotTruncateCommittedEntriesDuringRedelivery(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline.
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeHeartbeat1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: nil})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 2, entries: entries})
aeHeartbeat2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 3, pterm: 1, pindex: 3, entries: nil})
// Initial case is simple, just store the entry.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err := n.loadEntry(1)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Deliver a message.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.wal.State().Msgs, 2)
entry, err = n.loadEntry(2)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Heartbeat, makes sure commit moves up.
n.processAppendEntry(aeHeartbeat1, n.aesub)
require_Equal(t, n.commit, 2)
// Deliver another message.
n.processAppendEntry(aeMsg3, n.aesub)
require_Equal(t, n.wal.State().Msgs, 3)
entry, err = n.loadEntry(3)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Simulate receiving an old entry as a redelivery. We should not truncate as that lowers our commit.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.commit, 2)
// Heartbeat, makes sure we commit.
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_Equal(t, n.commit, 3)
}
func TestNRGCatchupFromNewLeaderWithIncorrectPterm(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline.
aeMsg := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 0, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
// Heartbeat, triggers catchup.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 0) // Commit should not change, as we missed an item.
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 0) // n.pterm
require_Equal(t, n.catchup.pindex, 0) // n.pindex
// First catchup message has the incorrect pterm, stop catchup and re-trigger later with the correct pterm.
n.processAppendEntry(aeMsg, n.catchup.sub)
require_True(t, n.catchup == nil)
require_Equal(t, n.pterm, 1)
require_Equal(t, n.pindex, 0)
// Heartbeat, triggers catchup.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 0) // Commit should not change, as we missed an item.
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 1) // n.pterm
require_Equal(t, n.catchup.pindex, 0) // n.pindex
// Now we get the message again and can continue to store it.
n.processAppendEntry(aeMsg, n.catchup.sub)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err := n.loadEntry(1)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Now heartbeat is able to commit the entry.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 1)
}
func TestNRGDontRemoveSnapshotIfTruncateToApplied(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline.
aeMsg := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
// Initial case is simple, just store the entry.
n.processAppendEntry(aeMsg, n.aesub)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err := n.loadEntry(1)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Heartbeat, makes sure commit moves up.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 1)
require_Equal(t, n.pterm, 1)
// Simulate upper layer calling down to apply.
n.Applied(1)
// Install snapshot and check it exists.
err = n.InstallSnapshot(nil, false)
require_NoError(t, err)
snapshots := path.Join(n.sd, snapshotsDir)
files, err := os.ReadDir(snapshots)
require_NoError(t, err)
require_Equal(t, len(files), 1)
// Truncate and check snapshot is kept.
n.truncateWAL(n.pterm, n.applied)
files, err = os.ReadDir(snapshots)
require_NoError(t, err)
require_Equal(t, len(files), 1)
}
func TestNRGSnapshotAndTruncateToApplied(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats1 := "yrzKKRBu" // "nats-1"
nats0 := "S1Nunr6R" // "nats-0"
n.Lock()
n.addPeer(nats1)
n.Unlock()
// Timeline, other leader
aeMsg1 := encode(t, &appendEntry{leader: nats1, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats1, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
// Timeline, we temporarily became leader
aeHeartbeat1 := encode(t, &appendEntry{leader: nats0, term: 2, commit: 3, pterm: 1, pindex: 3, entries: nil})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 2, commit: 3, pterm: 1, pindex: 3, entries: entries})
// Timeline, old leader is back.
aeHeartbeat2 := encode(t, &appendEntry{leader: nats1, term: 3, commit: 3, pterm: 1, pindex: 3, entries: nil})
// Simply receive first message.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.commit, 0)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err := n.loadEntry(1)
require_NoError(t, err)
require_Equal(t, entry.leader, nats1)
// Receive second message, which commits the first message.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.commit, 1)
require_Equal(t, n.wal.State().Msgs, 2)
entry, err = n.loadEntry(2)
require_NoError(t, err)
require_Equal(t, entry.leader, nats1)
// Simulate upper layer calling down to apply.
n.Applied(1)
// Send heartbeat, which commits the second message.
n.switchToLeader()
n.term = aeHeartbeat1.term
n.processAppendEntryResponse(&appendEntryResponse{
term: aeHeartbeat1.term,
index: aeHeartbeat1.pindex,
peer: nats1,
reply: _EMPTY_,
success: true,
})
require_Equal(t, n.commit, 3)
// Simulate upper layer calling down to apply.
n.Applied(3)
// Install snapshot and check it exists.
err = n.InstallSnapshot(nil, false)
require_NoError(t, err)
snapshots := path.Join(n.sd, snapshotsDir)
files, err := os.ReadDir(snapshots)
require_NoError(t, err)
require_Equal(t, len(files), 1)
require_Equal(t, n.wal.State().Msgs, 0)
// Store a third message, it stays uncommitted.
require_NoError(t, n.storeToWAL(aeMsg3))
require_Equal(t, n.commit, 3)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err = n.loadEntry(4)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Receive heartbeat from new leader, should not lose commits.
n.stepdown(noLeader)
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_Equal(t, n.wal.State().Msgs, 0)
require_Equal(t, n.commit, 3)
require_Equal(t, n.applied, 3)
}
func TestNRGIgnoreDoubleSnapshot(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 2, commit: 1, pterm: 1, pindex: 1, entries: entries})
// Simply receive first message.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 0)
// Heartbeat moves commit up.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 1)
// Manually call back down to applied.
n.Applied(1)
// Second message just for upping the pterm.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 2)
require_Equal(t, n.commit, 1)
require_Equal(t, n.pterm, 2)
require_Equal(t, n.term, 2)
// Snapshot, and confirm state.
err := n.InstallSnapshot(nil, false)
require_NoError(t, err)
snap, err := n.loadLastSnapshot()
require_NoError(t, err)
require_Equal(t, snap.lastTerm, 1)
require_Equal(t, snap.lastIndex, 1)
// Snapshot again, should not overwrite previous snapshot.
err = n.InstallSnapshot(nil, false)
require_Error(t, err, errNoSnapAvailable)
snap, err = n.loadLastSnapshot()
require_NoError(t, err)
require_Equal(t, snap.lastTerm, 1)
require_Equal(t, snap.lastIndex, 1)
}
func TestNRGDontSwitchToCandidateWithInflightSnapshot(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample snapshot entry, the content doesn't matter.
snapshotEntries := []*Entry{
newEntry(EntrySnapshot, nil),
newEntry(EntryPeerState, encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt})),
}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline.
aeTriggerCatchup := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
aeCatchupSnapshot := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: snapshotEntries})
// Switch follower into catchup.
n.processAppendEntry(aeTriggerCatchup, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 0) // n.pterm
require_Equal(t, n.catchup.pindex, 0) // n.pindex
// Follower receives a snapshot, marking a snapshot as inflight as the apply queue is async.
n.processAppendEntry(aeCatchupSnapshot, n.catchup.sub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 1)
// Try to switch to candidate, it should be blocked since the snapshot is not processed yet.
n.switchToCandidate()
require_Equal(t, n.State(), Follower)
// Simulate snapshot being processed by the upper layer.
n.Applied(1)
// Retry becoming candidate, snapshot is processed so can now do so.
n.switchToCandidate()
require_Equal(t, n.State(), Candidate)
}
func TestNRGDontSwitchToCandidateWithMultipleInflightSnapshots(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample snapshot entry, the content doesn't matter.
snapshotEntries := []*Entry{
newEntry(EntrySnapshot, nil),
newEntry(EntryPeerState, encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt})),
}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline.
aeSnapshot1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: snapshotEntries})
aeSnapshot2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: snapshotEntries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: nil})
// Simulate snapshots being sent to us.
n.processAppendEntry(aeSnapshot1, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 0)
require_Equal(t, n.applied, 0)
n.processAppendEntry(aeSnapshot2, n.aesub)
require_Equal(t, n.pindex, 2)
require_Equal(t, n.commit, 1)
require_Equal(t, n.applied, 0)
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.pindex, 2)
require_Equal(t, n.commit, 2)
require_Equal(t, n.applied, 0)
for i := uint64(1); i <= 2; i++ {
// Try to switch to candidate, it should be blocked since the snapshot is not processed yet.
n.switchToCandidate()
require_Equal(t, n.State(), Follower)
// Simulate snapshot being processed by the upper layer.
n.Applied(i)
}
// Retry becoming candidate, all snapshots processed so can now do so.
n.switchToCandidate()
require_Equal(t, n.State(), Candidate)
}
func TestNRGRecoverPindexPtermOnlyIfLogNotEmpty(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
gn := rg[0].(*stateAdder)
rn := rg[0].node().(*raft)
// Delete the msgs and snapshots, leaving the only remaining trace
// of the term in the TAV file.
store := filepath.Join(gn.cfg.Store)
require_NoError(t, rn.wal.Truncate(0))
require_NoError(t, os.RemoveAll(filepath.Join(store, "msgs")))
require_NoError(t, os.RemoveAll(filepath.Join(store, "snapshots")))
for _, gn := range rg {
gn.stop()
}
rg[0].restart()
rn = rg[0].node().(*raft)
// Both should be zero as, without any snapshots or log entries,
// the log is considered empty and therefore we account as such.
require_Equal(t, rn.pterm, 0)
require_Equal(t, rn.pindex, 0)
}
func TestNRGCancelCatchupWhenDetectingHigherTermDuringVoteRequest(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline.
aeCatchupTrigger := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
// Truncate to simulate we missed one message and need to catchup.
n.processAppendEntry(aeCatchupTrigger, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 0) // n.pterm
require_Equal(t, n.catchup.pindex, 0) // n.pindex
// Process first message as part of the catchup.
catchupSub := n.catchup.sub
n.processAppendEntry(aeMsg1, catchupSub)
require_True(t, n.catchup != nil)
// Receiving a vote request should cancel our catchup.
// Otherwise, we could receive catchup messages after this that provides the previous leader with quorum.
// If the new leader doesn't have these entries, the previous leader would desync since it would commit them.
err := n.processVoteRequest(&voteRequest{2, 1, 1, nats0, "reply"})
require_NoError(t, err)
require_True(t, n.catchup == nil)
}
func TestNRGMultipleStopsDontPanic(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
defer func() {
p := recover()
require_True(t, p == nil)
}()
for i := 0; i < 10; i++ {
n.Stop()
}
}
func TestNRGTruncateDownToCommitted(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
nats1 := "yrzKKRBu" // "nats-1"
// Timeline, we are leader
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
// Timeline, after leader change
aeMsg3 := encode(t, &appendEntry{leader: nats1, term: 2, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats1, term: 2, commit: 2, pterm: 2, pindex: 2, entries: nil})
// Simply receive first message.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.commit, 0)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err := n.loadEntry(1)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Receive second message, which commits the first message.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.commit, 1)
require_Equal(t, n.wal.State().Msgs, 2)
entry, err = n.loadEntry(2)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// We receive an entry from another leader, should truncate down to commit / remove the second message.
// After doing so, we should also be able to immediately store the message after.
n.processAppendEntry(aeMsg3, n.aesub)
require_Equal(t, n.commit, 1)
require_Equal(t, n.wal.State().Msgs, 2)
entry, err = n.loadEntry(2)
require_NoError(t, err)
require_Equal(t, entry.leader, nats1)
// Heartbeat moves commit up.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 2)
}
type mockWALTruncateAlwaysFails struct {
WAL
}
func (m mockWALTruncateAlwaysFails) Truncate(seq uint64) error {
return errors.New("test: truncate always fails")
}
func TestNRGTruncateDownToCommittedWhenTruncateFails(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
n.Lock()
n.wal = mockWALTruncateAlwaysFails{n.wal}
n.Unlock()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
nats1 := "yrzKKRBu" // "nats-1"
// Timeline, we are leader
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
// Timeline, after leader change
aeMsg3 := encode(t, &appendEntry{leader: nats1, term: 2, commit: 1, pterm: 1, pindex: 1, entries: entries})
// Simply receive first message.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.commit, 0)
require_Equal(t, n.wal.State().Msgs, 1)
entry, err := n.loadEntry(1)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// Receive second message, which commits the first message.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.commit, 1)
require_Equal(t, n.wal.State().Msgs, 2)
entry, err = n.loadEntry(2)
require_NoError(t, err)
require_Equal(t, entry.leader, nats0)
// We receive an entry from another leader, should truncate down to commit / remove the second message.
// But, truncation fails so should register that and not change pindex/pterm.
bindex, bterm := n.pindex, n.pterm
n.processAppendEntry(aeMsg3, n.aesub)
require_Error(t, n.werr, errors.New("test: truncate always fails"))
require_Equal(t, bindex, n.pindex)
require_Equal(t, bterm, n.pterm)
}
func TestNRGForwardProposalResponse(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
nc, _ := jsClientConnect(t, c.leader(), nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
n := rg.nonLeader().node().(*raft)
psubj := n.psubj
data := make([]byte, binary.MaxVarintLen64)
dn := binary.PutVarint(data, int64(123))
_, err := nc.Request(psubj, data[:dn], time.Second*5)
require_NoError(t, err)
rg.waitOnTotal(t, 123)
}
func TestNRGMemoryWALEmptiesSnapshotsDir(t *testing.T) {
n, c := initSingleMemRaftNodeWithCluster(t)
defer c.shutdown()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
// Simply receive first message.
n.processAppendEntry(aeMsg, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 0)
// Heartbeat moves commit up.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.commit, 1)
// Manually call back down to applied, and then snapshot.
n.Applied(1)
err := n.InstallSnapshot(nil, false)
require_NoError(t, err)
// Stop current node and restart it.
n.Stop()
n.WaitForStop()
s := c.servers[0]
ms, err := newMemStore(&StreamConfig{Name: "TEST", Storage: MemoryStorage})
require_NoError(t, err)
cfg := &RaftConfig{Name: "TEST", Store: n.sd, Log: ms}
n, err = s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// Since the WAL is in-memory, the snapshots dir should've been emptied upon restart.
files, err := os.ReadDir(filepath.Join(n.sd, snapshotsDir))
require_NoError(t, err)
require_Len(t, len(files), 0)
}
func TestNRGHealthCheckWaitForCatchup(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
n.Lock()
n.addPeer(nats0)
n.Unlock()
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 3, pterm: 1, pindex: 3, entries: nil})
// Switch follower into catchup.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 0) // n.pterm
require_Equal(t, n.catchup.pindex, 0) // n.pindex
require_Equal(t, n.catchup.cterm, aeHeartbeat.term)
require_Equal(t, n.catchup.cindex, aeHeartbeat.pindex)
// Catchup first message.
n.processAppendEntry(aeMsg1, n.catchup.sub)
require_Equal(t, n.pindex, 1)
require_False(t, n.Healthy())
// Catchup second message.
n.processAppendEntry(aeMsg2, n.catchup.sub)
require_Equal(t, n.pindex, 2)
require_Equal(t, n.commit, 1)
require_False(t, n.Healthy())
// If we apply the entry sooner than we receive the next catchup message,
// should not mark as healthy since we're still in catchup.
n.Applied(1)
require_False(t, n.Healthy())
// Catchup third message.
n.processAppendEntry(aeMsg3, n.catchup.sub)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 2)
n.Applied(2)
require_False(t, n.Healthy())
// Heartbeat stops catchup.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_True(t, n.catchup == nil)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 3)
require_False(t, n.Healthy())
// Still need to wait for the last entry to be applied.
n.Applied(3)
require_True(t, n.Healthy())
}
func TestNRGHealthCheckWaitForDoubleCatchup(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
n.Lock()
n.addPeer(nats0)
n.Unlock()
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeHeartbeat1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: nil})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: entries})
aeHeartbeat2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 3, pterm: 1, pindex: 3, entries: nil})
// Switch follower into catchup.
n.processAppendEntry(aeHeartbeat1, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 0) // n.pterm
require_Equal(t, n.catchup.pindex, 0) // n.pindex
require_Equal(t, n.catchup.cterm, aeHeartbeat1.term)
require_Equal(t, n.catchup.cindex, aeHeartbeat1.pindex)
// Catchup first message.
n.processAppendEntry(aeMsg1, n.catchup.sub)
require_Equal(t, n.pindex, 1)
require_False(t, n.Healthy())
// We miss this message, since we're catching up.
n.processAppendEntry(aeMsg3, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.pindex, 1)
require_False(t, n.Healthy())
// We also miss the heartbeat, since we're catching up.
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.pindex, 1)
require_False(t, n.Healthy())
// Catchup second message, this will stop catchup.
n.processAppendEntry(aeMsg2, n.catchup.sub)
require_Equal(t, n.pindex, 2)
require_Equal(t, n.commit, 1)
n.Applied(1)
require_False(t, n.Healthy())
// We expect to still be in catchup, waiting for a heartbeat or new append entry to reset.
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.cterm, aeHeartbeat1.term)
require_Equal(t, n.catchup.cindex, aeHeartbeat1.pindex)
// We now get a 'future' heartbeat, should restart catchup.
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 1) // n.pterm
require_Equal(t, n.catchup.pindex, 2) // n.pindex
require_Equal(t, n.catchup.cterm, aeHeartbeat2.term)
require_Equal(t, n.catchup.cindex, aeHeartbeat2.pindex)
require_False(t, n.Healthy())
// Catchup third message.
n.processAppendEntry(aeMsg3, n.catchup.sub)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 2)
n.Applied(2)
require_False(t, n.Healthy())
// Heartbeat stops catchup.
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_True(t, n.catchup == nil)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 3)
require_False(t, n.Healthy())
// Still need to wait for the last entry to be applied.
n.Applied(3)
require_True(t, n.Healthy())
}
func TestNRGHealthCheckWaitForPendingCommitsWhenPaused(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
n.Lock()
n.addPeer(nats0)
n.Unlock()
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: nil})
// Process first message.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.pindex, 1)
require_False(t, n.Healthy())
// Process second message, moves commit up.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 2)
require_False(t, n.Healthy())
// We're healthy once we've applied the first message.
n.Applied(1)
require_True(t, n.Healthy())
// If we're paused we still are healthy if there are no pending commits.
err := n.PauseApply()
require_NoError(t, err)
require_True(t, n.Healthy())
// Heartbeat marks second message to be committed.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.pindex, 2)
require_False(t, n.Healthy())
// Resuming apply commits the message.
n.ResumeApply()
require_NoError(t, err)
require_False(t, n.Healthy())
// But still waiting for it to be applied before marking healthy.
n.Applied(2)
require_True(t, n.Healthy())
}
func TestNRGAppendEntryCanEstablishQuorumAfterLeaderChange(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
n.Lock()
n.addPeer(nats0)
n.Unlock()
// Timeline
aeMsg := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeHeartbeatResponse := &appendEntryResponse{term: 1, index: 2, peer: nats0, success: true}
// Process first message.
n.processAppendEntry(aeMsg, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.aflr, 0)
// Simulate becoming leader, and not knowing if the stored entry has quorum and can be committed.
// Switching to leader should send a heartbeat.
n.switchToLeader()
require_Equal(t, n.aflr, 2)
require_Equal(t, n.commit, 0)
// We simulate receiving the successful heartbeat response here. It should move the commit up.
n.processAppendEntryResponse(aeHeartbeatResponse)
require_Equal(t, n.commit, 2)
require_Equal(t, n.aflr, 2)
// Once the entry is applied, it should reset the applied floor.
n.Applied(2)
require_Equal(t, n.aflr, 0)
}
func TestNRGQuorumAccounting(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats1 := "yrzKKRBu" // "nats-1"
nats2 := "cnrtt3eg" // "nats-2"
n.Lock()
n.addPeer(nats1)
n.addPeer(nats2)
n.Unlock()
// Timeline
aeHeartbeat1Response := &appendEntryResponse{term: 1, index: 1, peer: nats1, success: true}
aeHeartbeat2Response := &appendEntryResponse{term: 1, index: 1, peer: nats2, success: true}
// Adjust cluster size, so we need at least 2 responses from other servers to establish quorum.
require_NoError(t, n.AdjustBootClusterSize(5))
require_Equal(t, n.csz, 5)
require_Equal(t, n.qn, 3)
// Switch this node to leader which sends an entry.
n.switchToLeader()
n.term = 1
require_Equal(t, n.pindex, 1)
// The first response MUST NOT indicate quorum has been reached.
n.processAppendEntryResponse(aeHeartbeat1Response)
require_Equal(t, n.commit, 0)
// The second response means we have reached quorum and can move commit up.
n.processAppendEntryResponse(aeHeartbeat2Response)
require_Equal(t, n.commit, 1)
}
func TestNRGRevalidateQuorumAfterLeaderChange(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats1 := "yrzKKRBu" // "nats-1"
nats2 := "cnrtt3eg" // "nats-2"
n.Lock()
n.addPeer(nats1)
n.addPeer(nats2)
n.Unlock()
// Timeline
aeHeartbeat1Response := &appendEntryResponse{term: 1, index: 1, peer: nats1, success: true}
aeHeartbeat2Response := &appendEntryResponse{term: 6, index: 2, peer: nats2, success: true}
// Adjust cluster size, so we need at least 2 responses from other servers to establish quorum.
require_NoError(t, n.AdjustBootClusterSize(5))
require_Equal(t, n.csz, 5)
require_Equal(t, n.qn, 3)
// Switch this node to leader which sends an entry.
n.term++
n.switchToLeader()
require_Equal(t, n.term, 1)
require_Equal(t, n.pindex, 1)
// We have one server that signals the message was stored. The leader will add 1 to the acks count.
n.processAppendEntryResponse(aeHeartbeat1Response)
require_Equal(t, n.commit, 0)
require_Len(t, len(n.acks), 1)
// We stepdown now and don't know if we will have quorum on the first entry.
n.stepdown(noLeader)
// Let's assume there are a bunch of leader elections now, data being added to the log, being truncated, etc.
// We don't know what happened, maybe we were partitioned, but we can't know for sure if the first entry has quorum.
// We now become leader again.
n.term = 6
n.switchToLeader()
require_Equal(t, n.term, 6)
// We now receive a successful response from another server saying they have stored it.
// Anything can have happened to the replica that said success before, we can't assume that's still valid.
// So our commit must stay the same and we restart counting for quorum.
n.processAppendEntryResponse(aeHeartbeat2Response)
require_Equal(t, n.commit, 0)
require_Len(t, len(n.acks), 1)
}
func TestNRGSignalLeadChangeFalseIfCampaignImmediately(t *testing.T) {
tests := []struct {
title string
switchNode func(n *raft)
}{
{
title: "Follower",
},
{
title: "Candidate",
switchNode: func(n *raft) {
n.switchToCandidate()
},
},
{
title: "Leader",
switchNode: func(n *raft) {
n.switchToCandidate()
n.switchToLeader()
select {
case <-n.LeadChangeC():
t.Error("Expected no leadChange signal")
default:
// Expecting no signal yet.
}
},
},
}
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
// Campaigning immediately signals we're the preferred leader.
require_NoError(t, n.CampaignImmediately())
if test.switchNode != nil {
test.switchNode(n)
}
n.processAppendEntry(aeMsg1, n.aesub)
select {
case isLeader := <-n.LeadChangeC():
require_False(t, isLeader)
default:
t.Error("Expected leadChange signal")
}
require_Equal(t, n.State(), Follower)
require_Equal(t, n.leader, nats0)
require_Equal(t, n.term, 1)
})
}
}
func TestNRGCatchupDontCountTowardQuorum(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
aeReply := "$TEST"
nc, err := nats.Connect(n.s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
require_NoError(t, err)
defer nc.Close()
sub, err := nc.SubscribeSync(aeReply)
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
// Timeline
aeMissedMsg := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries, reply: aeReply})
ae := appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries, reply: aeReply}
aeCatchupTrigger := encode(t, &ae)
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 2, entries: nil, reply: aeReply})
// Simulate we missed all messages up to this point.
n.processAppendEntry(aeCatchupTrigger, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 0) // n.pterm
require_Equal(t, n.catchup.pindex, 0) // n.pindex
require_Equal(t, n.catchup.cterm, ae.pterm)
require_Equal(t, n.catchup.cindex, ae.pindex)
// Should reply we require catchup.
msg, err := sub.NextMsg(time.Second)
require_NoError(t, err)
ar := decodeAppendEntryResponse(msg.Data)
require_Equal(t, ar.index, 0)
require_False(t, ar.success)
require_True(t, strings.HasPrefix(msg.Reply, "$NRG.CR"))
// Should NEVER respond to catchup messages.
n.processAppendEntry(aeMissedMsg, n.catchup.sub)
_, err = sub.NextMsg(time.Second)
require_Error(t, err, nats.ErrTimeout)
n.processAppendEntry(aeCatchupTrigger, n.catchup.sub)
_, err = sub.NextMsg(time.Second)
require_Error(t, err, nats.ErrTimeout)
// Now we've received all messages, stop catchup, and respond success to new message.
n.processAppendEntry(aeHeartbeat, n.aesub)
msg, err = sub.NextMsg(time.Second)
require_NoError(t, err)
ar = decodeAppendEntryResponse(msg.Data)
require_Equal(t, ar.index, aeHeartbeat.pindex)
require_True(t, ar.success)
require_Equal(t, msg.Reply, _EMPTY_)
}
func TestNRGIgnoreTrackResponseWhenNotLeader(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats1 := "yrzKKRBu" // "nats-1"
n.Lock()
n.addPeer(nats1)
// Keep quorum at 2 so the initial leader NOOP does not auto-commit.
n.lsut = time.Time{}
n.Unlock()
// Switch this node to leader which sends an entry.
n.term++
n.switchToLeader()
require_Equal(t, n.term, 1)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.pterm, 1)
require_Equal(t, n.commit, 0)
// Step down
n.stepdown(noLeader)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.pterm, 1)
require_Equal(t, n.commit, 0)
// Normally would commit the entry, but since we're not leader anymore we should ignore it.
n.trackResponse(&appendEntryResponse{1, 1, "peer", _EMPTY_, true})
require_Equal(t, n.commit, 0)
}
func TestNRGRejectNewAppendEntryFromPreviousLeader(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
// Accept first message because it equals our term.
n.term = 1
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.pterm, 1)
require_Equal(t, n.pindex, 1)
// We are part of the successful vote for a new leader under a new term.
require_NoError(t, n.processVoteRequest(&voteRequest{term: 5, lastTerm: 1, lastIndex: 2}))
// Must reject entry from a previous term.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pterm, 1)
require_Equal(t, n.pindex, 1)
}
func TestNRGRejectAppendEntryDuringCatchupFromPreviousLeader(t *testing.T) {
test := func(t *testing.T, isCatchingUp, oldBehavior bool) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
// Accept first message because it equals our term.
n.term = 1
n.processAppendEntry(aeMsg2, n.aesub)
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.pterm, 0) // n.pterm
require_Equal(t, n.catchup.pindex, 0) // n.pindex
require_Equal(t, n.catchup.cterm, aeMsg2.pterm)
require_Equal(t, n.catchup.cindex, aeMsg2.pindex)
// Under the new behavior the term of the leader that's doing the catchup is included.
if !oldBehavior {
aeMsg1.lterm = 1
aeMsg2.lterm = 1
}
// First catchup message is accepted.
catchup := n.catchup
n.processAppendEntry(aeMsg1, catchup.sub)
require_Equal(t, n.pterm, 1)
require_Equal(t, n.pindex, 1)
// We are part of the successful vote for a new leader under a new term.
require_NoError(t, n.processVoteRequest(&voteRequest{term: 5, lastTerm: 1, lastIndex: 2}))
// Voting cancels catchup. For testing, revert that so we can test
// what a catchup message after upping the term does.
nsub := n.aesub
if isCatchingUp {
n.catchup = catchup
nsub = catchup.sub
}
// Now send the second catchup entry.
n.processAppendEntry(aeMsg2, nsub)
require_Equal(t, n.pterm, 1)
// Under the old behavior this entry is wrongly accepted.
// A new server will also know to be backward-compatible if being caught up by an old server.
if isCatchingUp && oldBehavior {
require_Equal(t, n.pindex, 2)
} else {
// Under the new behavior the entry is correctly accepted.
require_Equal(t, n.pindex, 1)
}
}
for _, isCatchingUp := range []bool{false, true} {
for _, oldBehavior := range []bool{false, true} {
title := "new-entry"
if isCatchingUp {
title = "catchup"
}
if oldBehavior {
title += "-backward-compatible"
}
t.Run(title, func(t *testing.T) { test(t, isCatchingUp, oldBehavior) })
}
}
}
func TestNRGDontRejectAppendEntryFromReplay(t *testing.T) {
test := func(t *testing.T, restart bool) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Stop all servers except the leader.
l := rg.leader().(*stateAdder)
rs := rg.nonLeader()
for _, sm := range rg {
if sm != l {
sm.stop()
}
}
// Propose a new entry to the leader and confirm it's stored in its log.
pindex, _, _ := l.node().Progress()
l.proposeDelta(10)
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
if index, _, _ := l.node().Progress(); index == pindex {
return errors.New("proposal not stored yet")
}
return nil
})
// Optionally restart the leader, which will require replay of the entries in the log.
if restart {
l.stop()
l.restart()
}
// Shutdown server should be caught up.
rs.restart()
// Wait for both online servers to have applied the delta.
checkFor(t, 5*time.Second, 200*time.Millisecond, func() (err error) {
for _, sm := range rg {
if sm != l && sm != rs {
break
}
if total := sm.(*stateAdder).total(); total != 10 {
err = errors.Join(err, fmt.Errorf("expected 10 total, got: %d", total))
}
}
return err
})
}
t.Run("no-restart", func(t *testing.T) { test(t, false) })
t.Run("with-restart", func(t *testing.T) { test(t, true) })
}
func TestNRGSimpleCatchup(t *testing.T) {
test := func(t *testing.T, leaderChange bool) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
// Shutdown a single group, we'll start it later for catchup.
nl := rg.nonLeader()
nl.stop()
l := rg.leader().(*stateAdder)
l.proposeDelta(10)
// Wait for remaining servers to have applied the delta.
checkFor(t, 2*time.Second, 200*time.Millisecond, func() (err error) {
for _, sm := range rg {
total := sm.(*stateAdder).total()
if sm == nl && total != 0 {
err = errors.Join(err, errors.New("expected shutdown server to not get data"))
} else if sm != nl && total != 10 {
err = errors.Join(err, fmt.Errorf("expected 10 total, got: %d", total))
}
}
return err
})
if leaderChange {
require_NoError(t, l.node().StepDown())
}
// Shutdown server should be caught up.
nl.restart()
rg.waitOnTotal(t, 10)
}
t.Run("same-leader", func(t *testing.T) { test(t, false) })
t.Run("change-leader", func(t *testing.T) { test(t, true) })
}
func TestNRGSnapshotCatchup(t *testing.T) {
test := func(t *testing.T, restart bool) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
l := rg.leader().(*stateAdder)
var s1 stateMachine
var s2 stateMachine
for _, sm := range rg {
if sm == l {
continue
}
if s1 == nil {
s1 = sm
} else {
s2 = sm
}
}
// Stop one non-leader server.
s1.stop()
// Wait for both online servers to have applied the delta.
l.proposeDelta(10)
checkFor(t, 5*time.Second, 200*time.Millisecond, func() (err error) {
for _, sm := range rg {
if sm == s1 {
continue
}
if total := sm.(*stateAdder).total(); total != 10 {
err = errors.Join(err, fmt.Errorf("expected 10 total, got: %d", total))
}
}
return err
})
// Shutdown last non-leader server.
s2.stop()
// Snapshot so outdated server needs to catchup based on it.
l.snapshot(t)
// Propose a new entry to the leader and confirm it's stored in its log.
pindex, _, _ := l.node().Progress()
l.proposeDelta(10)
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
if index, _, _ := l.node().Progress(); index == pindex {
return errors.New("proposal not stored yet")
}
return nil
})
// Optionally restart the leader, which will require replay of the entries in the log.
if restart {
l.stop()
l.restart()
}
// Shutdown server should be caught up.
s1.restart()
// Wait for both online servers to have applied the delta.
checkFor(t, 5*time.Second, 200*time.Millisecond, func() (err error) {
for _, sm := range rg {
if sm == s2 {
break
}
if total := sm.(*stateAdder).total(); total != 20 {
err = errors.Join(err, fmt.Errorf("expected 20 total, got: %d", total))
}
}
return err
})
}
t.Run("no-restart", func(t *testing.T) { test(t, false) })
t.Run("with-restart", func(t *testing.T) { test(t, true) })
}
func TestNRGSnapshotRecovery(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 0, pindex: 0, entries: entries})
// Store one entry.
n.processAppendEntry(aeMsg, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 1)
require_Equal(t, n.applied, 0)
// Apply it.
n.Applied(1)
require_Equal(t, n.applied, 1)
// Install the snapshot.
require_NoError(t, n.InstallSnapshot(nil, false))
// Restoring the snapshot should not up applied, because the apply queue is async.
n.pindex, n.commit, n.processed, n.applied = 0, 0, 0, 0
n.setupLastSnapshot()
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 1)
require_Equal(t, n.processed, 0)
require_Equal(t, n.applied, 0)
}
func TestNRGLoadLastSnapshotCleansLegacyZeroIndexSnapshot(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
snapDir := filepath.Join(n.sd, snapshotsDir)
require_NoError(t, os.MkdirAll(snapDir, defaultDirPerms))
legacy := &snapshot{
lastTerm: 1,
lastIndex: 0,
peerstate: encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt}),
data: []byte("legacy"),
}
sfile := filepath.Join(snapDir, fmt.Sprintf(snapFileT, legacy.lastTerm, legacy.lastIndex))
require_NoError(t, writeFileWithSync(sfile, n.encodeSnapshot(legacy), defaultFilePerms))
n.Lock()
n.snapfile = sfile
snap, err := n.loadLastSnapshot()
snapfile := n.snapfile
n.Unlock()
require_Error(t, err, errNoSnapAvailable)
require_True(t, snap == nil)
require_Equal(t, snapfile, _EMPTY_)
_, err = os.Stat(sfile)
require_True(t, os.IsNotExist(err))
}
func TestNRGSetupLastSnapshotIgnoresTmpFile(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Make a snapshot file
snapDir := filepath.Join(n.sd, snapshotsDir)
require_NoError(t, os.MkdirAll(snapDir, defaultDirPerms))
snap := &snapshot{
lastTerm: 1,
lastIndex: 1,
peerstate: encodePeerState(n.currentPeerState()),
data: []byte("snapshot"),
}
sfile := filepath.Join(snapDir, fmt.Sprintf(snapFileT, snap.lastTerm, snap.lastIndex))
require_NoError(t, os.WriteFile(sfile, n.encodeSnapshot(snap), defaultFilePerms))
// Make a empty snapshot file with a higher index, and extension
// .tmp to simulate the intermediate step in writeAtomically.
tmpSnap := filepath.Join(snapDir, fmt.Sprintf(snapFileT, uint64(1), uint64(2))+".tmp")
require_NoError(t, os.WriteFile(tmpSnap, nil, defaultFilePerms))
// If bug is present: setupLastSnapshot() selects
// the higher numbered snapshot file, even if it
// has the .tmp extension. The presence of a .tmp
// snapshot file means the server crashed, preventing
// writeAtomically() to finish. The contents of a
// .tmp snapshot file may be incomplete.
// Verify that setupLastSnapshot selects the "good"
// snapshot.
err := n.setupLastSnapshot()
require_NoError(t, err)
require_Equal(t, n.snapfile, sfile)
}
func TestNRGKeepRunningOnServerShutdown(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
n.RLock()
s := n.s
wal := n.wal.(*memStore)
n.RUnlock()
n.wg.Add(1)
s.startGoRoutine(n.run, nil)
s.running.Store(false)
time.Sleep(time.Second)
wal.mu.RLock()
msgs := wal.msgs
wal.mu.RUnlock()
require_NotNil(t, msgs)
n.Stop()
n.WaitForStop()
wal.mu.RLock()
msgs = wal.msgs
wal.mu.RUnlock()
require_True(t, msgs == nil)
}
func TestNRGVoteResponseEncoding(t *testing.T) {
vr := &voteResponse{term: 1, peer: "S1Nunr6R"}
vr.granted, vr.empty = false, false
res := vr.encode()
require_Equal(t, res[16], 0)
require_True(t, reflect.DeepEqual(decodeVoteResponse(res), vr))
vr.granted, vr.empty = true, false
res = vr.encode()
require_Equal(t, res[16], 1)
require_True(t, reflect.DeepEqual(decodeVoteResponse(res), vr))
vr.granted, vr.empty = false, true
res = vr.encode()
require_Equal(t, res[16], 2)
require_True(t, reflect.DeepEqual(decodeVoteResponse(res), vr))
vr.granted, vr.empty = true, true
res = vr.encode()
require_Equal(t, res[16], 3)
require_True(t, reflect.DeepEqual(decodeVoteResponse(res), vr))
}
func TestNRGInitializeAndScaleUp(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 0, pindex: 0, entries: entries})
require_True(t, n.initializing)
n.scaleUp = true
n.SetObserver(true)
require_Equal(t, n.term, 0)
voteReply := "$TEST"
nc, err := nats.Connect(n.s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
require_NoError(t, err)
defer nc.Close()
sub, err := nc.SubscribeSync(voteReply)
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
// Votes on a new leader, and resets notion of "empty vote" to ease getting quorum.
require_NoError(t, n.processVoteRequest(&voteRequest{term: 1, candidate: nats0, reply: voteReply}))
require_Equal(t, n.term, 1)
require_Equal(t, n.vote, nats0)
require_True(t, n.initializing)
require_True(t, n.scaleUp)
require_True(t, n.observer)
msg, err := sub.NextMsg(time.Second)
require_NoError(t, err)
vr := decodeVoteResponse(msg.Data)
require_True(t, vr.granted)
require_False(t, vr.empty)
// Processing an append entry resets scale up and puts us out of observer mode.
n.processAppendEntry(aeMsg, n.aesub)
require_Equal(t, n.pindex, 1)
require_False(t, n.initializing)
require_False(t, n.scaleUp)
require_False(t, n.observer)
// Simulate a reset.
n.resetWAL()
require_Equal(t, n.pindex, 0)
// Vote after a WAL reset must be an "empty vote".
require_NoError(t, n.processVoteRequest(&voteRequest{term: 2, candidate: "_random_", reply: voteReply}))
require_Equal(t, n.term, 2)
require_Equal(t, n.vote, "_random_")
// Vote is still granted, but now marked as "empty".
msg, err = sub.NextMsg(time.Second)
require_NoError(t, err)
vr = decodeVoteResponse(msg.Data)
require_True(t, vr.granted)
require_True(t, vr.empty)
// Reset to check if snapshot on catchup can also reset this.
n.initializing = true
n.scaleUp = true
n.SetObserver(true)
snapshotEntries := []*Entry{
newEntry(EntrySnapshot, nil),
newEntry(EntryPeerState, encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt})),
}
aeSnapshot := encode(t, &appendEntry{leader: nats0, term: 2, commit: 1, pterm: 1, pindex: 1, entries: snapshotEntries})
n.createCatchup(aeSnapshot)
n.processAppendEntry(aeSnapshot, n.catchup.sub)
require_False(t, n.initializing)
require_False(t, n.scaleUp)
require_False(t, n.observer)
// Simulate a reset.
n.resetWAL()
require_Equal(t, n.pindex, 0)
// Vote when initializing but not scaling up, must also NOT be an "empty vote".
n.initializing = true
require_NoError(t, n.processVoteRequest(&voteRequest{term: 3, candidate: "_random_", reply: voteReply}))
require_Equal(t, n.term, 3)
require_Equal(t, n.vote, "_random_")
// Vote is still granted, but now marked as "empty".
msg, err = sub.NextMsg(time.Second)
require_NoError(t, err)
vr = decodeVoteResponse(msg.Data)
require_True(t, vr.granted)
require_False(t, vr.empty)
}
func TestNRGReplayOnSnapshotSameTerm(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 2, entries: entries})
// Process the first append entry.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.pindex, 1)
// Commit and apply.
require_NoError(t, n.applyCommit(1))
require_Equal(t, n.commit, 1)
n.Applied(1)
require_Equal(t, n.applied, 1)
// Install snapshot.
require_NoError(t, n.InstallSnapshot(nil, false))
snap, err := n.loadLastSnapshot()
require_NoError(t, err)
require_Equal(t, snap.lastIndex, 1)
// Process other messages.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 2)
n.processAppendEntry(aeMsg3, n.aesub)
require_Equal(t, n.pindex, 3)
// Replay the append entry that matches our snapshot.
// This can happen as a repeated entry, or a delayed append entry after having already received it in a catchup.
// Should be recognized as a replay with the same term, marked as success, and not truncate.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 3)
}
func TestNRGReplayOnSnapshotDifferentTerm(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 0, pindex: 0, entries: entries, lterm: 2})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 2, commit: 1, pterm: 1, pindex: 1, entries: entries, lterm: 2})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 2, commit: 1, pterm: 2, pindex: 2, entries: entries, lterm: 2})
// Process the first append entry.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.pindex, 1)
// Commit and apply.
require_NoError(t, n.applyCommit(1))
require_Equal(t, n.commit, 1)
n.Applied(1)
require_Equal(t, n.applied, 1)
// Install snapshot.
require_NoError(t, n.InstallSnapshot(nil, false))
snap, err := n.loadLastSnapshot()
require_NoError(t, err)
require_Equal(t, snap.lastIndex, 1)
// Reset applied to simulate having received the snapshot from
// another leader, and we didn't apply yet since it's async.
n.applied = 0
// Process other messages.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 2)
n.processAppendEntry(aeMsg3, n.aesub)
require_Equal(t, n.pindex, 3)
// Replay the append entry that matches our snapshot.
// This can happen as a repeated entry, or a delayed append entry after having already received it in a catchup.
// Should be recognized as truncating back to the installed snapshot, not reset the WAL fully.
// Since all is aligned after truncation, should also be able to apply the entry.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 2)
// Should now also be able to apply the third entry.
n.processAppendEntry(aeMsg3, n.aesub)
require_Equal(t, n.pindex, 3)
}
func TestNRGSizeAndApplied(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
nats0 := "S1Nunr6R" // "nats-0"
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: []*Entry{newEntry(EntryNormal, esm)}})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: []*Entry{newEntry(EntryNormal, esm)}})
var (
entries uint64
bytes uint64
)
// Initially our WAL is empty.
entries, bytes = n.Size()
require_Equal(t, entries, 0)
require_Equal(t, bytes, 0)
// Store the first append entry.
require_NoError(t, n.storeToWAL(aeMsg1))
entries, bytes = n.Size()
require_Equal(t, entries, 1)
require_Equal(t, bytes, 105)
// Store the second append entry.
require_NoError(t, n.storeToWAL(aeMsg2))
entries, bytes = n.Size()
require_Equal(t, entries, 2)
require_Equal(t, bytes, 210)
// Applying should return what part of the WAL can be compacted.
n.commit = 1
entries, bytes = n.Applied(1)
require_Equal(t, entries, 1)
require_Equal(t, bytes, 105)
// After applying all should return our whole WAL can be compacted.
n.commit = 2
entries, bytes = n.Applied(2)
require_Equal(t, entries, 2)
require_Equal(t, bytes, 210)
// Installing a snapshot should properly correct n.papplied and n.bytes
n.applied = 1 // Reset just for testing.
require_NoError(t, n.InstallSnapshot(nil, false))
require_Equal(t, n.papplied, 1)
require_Equal(t, n.bytes, 105)
entries, bytes = n.Size()
require_Equal(t, entries, 1)
require_Equal(t, bytes, 105)
entries, bytes = n.Applied(2)
require_Equal(t, entries, 1)
require_Equal(t, bytes, 105)
require_NoError(t, n.InstallSnapshot(nil, false))
require_Equal(t, n.papplied, 2)
require_Equal(t, n.bytes, 0)
entries, bytes = n.Size()
require_Equal(t, entries, 0)
require_Equal(t, bytes, 0)
}
func TestNRGIgnoreEntryAfterCanceledCatchup(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
n.processAppendEntry(aeMsg2, n.aesub)
require_True(t, n.catchup != nil)
csub := n.catchup.sub
n.cancelCatchup()
// Catchup was canceled, a message on this canceled catchup should not be stored.
n.processAppendEntry(aeMsg1, csub)
require_Equal(t, n.pindex, 0)
}
func TestNRGDelayedMessagesAfterCatchupDontCountTowardQuorum(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
aeReply := "$TEST"
nats0 := "S1Nunr6R" // "nats-0"
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries, reply: aeReply})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries, reply: aeReply})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 2, entries: entries, reply: aeReply})
// Triggers catchup.
n.processAppendEntry(aeMsg3, n.aesub)
require_True(t, n.catchup != nil)
// Catchup runs partially.
n.processAppendEntry(aeMsg1, n.catchup.sub)
n.processAppendEntry(aeMsg2, n.catchup.sub)
require_Equal(t, n.pindex, 2)
require_Equal(t, n.commit, 1)
n.Applied(1)
require_Equal(t, n.applied, 1)
require_NoError(t, n.InstallSnapshot(nil, false))
nc, err := nats.Connect(n.s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
require_NoError(t, err)
defer nc.Close()
sub, err := nc.SubscribeSync(aeReply)
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
// We now receive delayed "real-time" messages.
// The first message needs to be a copy, because we've committed it before and returned it to the pool.
aeMsg1Copy := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries, reply: aeReply})
n.processAppendEntry(aeMsg1Copy, n.aesub)
require_Equal(t, n.pindex, 2)
// Should NOT reply "success", otherwise we would wrongfully provide quorum while not having an up-to-date log.
_, err = sub.NextMsg(500 * time.Millisecond)
require_Error(t, err, nats.ErrTimeout)
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 2)
// Should NOT reply "success", otherwise we would wrongfully provide quorum while not having an up-to-date log.
_, err = sub.NextMsg(500 * time.Millisecond)
require_Error(t, err, nats.ErrTimeout)
n.processAppendEntry(aeMsg3, n.aesub)
require_Equal(t, n.pindex, 3)
// Should reply "success", this is the latest message.
msg, err := sub.NextMsg(500 * time.Millisecond)
require_NoError(t, err)
ar := decodeAppendEntryResponse(msg.Data)
require_Equal(t, ar.index, 3)
require_True(t, ar.success)
require_Equal(t, msg.Reply, _EMPTY_)
}
func TestNRGStepdownWithHighestTermDuringCatchup(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
aeMsg1 := encode(t, &appendEntry{leader: nats0, lterm: 10, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, lterm: 20, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
// Need to store the message, stepdown, and up term.
n.switchToCandidate()
require_Equal(t, n.term, 1)
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.term, 10)
require_Equal(t, n.pindex, 1)
// Need to store the message, stepdown, and up term.
n.switchToLeader()
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.term, 20)
require_Equal(t, n.pindex, 2)
}
func TestNRGTruncateOnStartup(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
s := c.servers[0] // RunBasicJetStreamServer not available
defer c.shutdown()
storeDir := t.TempDir()
fcfg := FileStoreConfig{StoreDir: storeDir, BlockSize: defaultMediumBlockSize, AsyncFlush: false, srv: s}
scfg := StreamConfig{Name: "RAFT", Storage: FileStorage}
fs, err := newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg := &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
err = s.bootstrapRaftNode(cfg, nil, false)
require_NoError(t, err)
n, err := s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
// Store two messages the normal way.
n.processAppendEntry(aeMsg1, n.aesub)
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 2)
state := n.wal.State()
require_Equal(t, state.Msgs, 2)
require_Equal(t, state.FirstSeq, 1)
require_Equal(t, state.LastSeq, 2)
require_Equal(t, state.NumDeleted, 0)
// Simulate a truncation that's only performed halfway, and we got hard killed at some point.
removed, err := n.wal.RemoveMsg(2)
require_True(t, removed)
require_NoError(t, err)
state = n.wal.State()
require_Equal(t, state.Msgs, 1)
require_Equal(t, state.FirstSeq, 1)
require_Equal(t, state.LastSeq, 2)
require_Equal(t, state.NumDeleted, 1)
// Restart.
n.Stop()
n.WaitForStop()
require_NoError(t, fs.Stop())
fs, err = newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg = &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
n, err = s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// Should truncate the WAL on startup, the message was removed.
state = n.wal.State()
require_Equal(t, state.Msgs, 1)
require_Equal(t, state.FirstSeq, 1)
require_Equal(t, state.LastSeq, 1)
require_Equal(t, state.NumDeleted, 0)
// Store an invalid append entry manually.
aeMsg2.pindex = 0
aeMsg2 = encode(t, aeMsg2)
_, _, err = n.wal.StoreMsg(_EMPTY_, nil, aeMsg2.buf, 0)
require_NoError(t, err)
state = n.wal.State()
require_Equal(t, state.Msgs, 2)
require_Equal(t, state.FirstSeq, 1)
require_Equal(t, state.LastSeq, 2)
require_Equal(t, state.NumDeleted, 0)
// Restart.
n.Stop()
n.WaitForStop()
require_NoError(t, fs.Stop())
fs, err = newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg = &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
n, err = s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// Should truncate the WAL on startup, the append entry is invalid.
state = n.wal.State()
require_Equal(t, state.Msgs, 1)
require_Equal(t, state.FirstSeq, 1)
require_Equal(t, state.LastSeq, 1)
require_Equal(t, state.NumDeleted, 0)
}
func TestNRGLeaderCatchupHandling(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 2, entries: entries})
n.processAppendEntry(aeMsg1, n.aesub)
n.processAppendEntry(aeMsg2, n.aesub)
n.processAppendEntry(aeMsg3, n.aesub)
require_Equal(t, n.pindex, 3)
n.switchToLeader()
catchupReply := "$TEST"
nc, err := nats.Connect(n.s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
require_NoError(t, err)
defer nc.Close()
sub, err := nc.SubscribeSync(catchupReply)
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
// Simulate a follower that's up-to-date with only the first message.
n.catchupFollower(&appendEntryResponse{success: false, term: 1, index: 1, reply: catchupReply})
// Should receive all messages the leader knows up to this point.
msg, err := sub.NextMsg(500 * time.Millisecond)
require_NoError(t, err)
ae, err := decodeAppendEntry(msg.Data, nil, _EMPTY_)
require_NoError(t, err)
require_Equal(t, ae.pterm, 1)
require_Equal(t, ae.pindex, 1)
msg, err = sub.NextMsg(500 * time.Millisecond)
require_NoError(t, err)
ae, err = decodeAppendEntry(msg.Data, nil, _EMPTY_)
require_NoError(t, err)
require_Equal(t, ae.pterm, 1)
require_Equal(t, ae.pindex, 2)
}
func TestNRGNewEntriesFromOldLeaderResetsWALDuringCatchup(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
aeMsg1 := encode(t, &appendEntry{leader: nats0, lterm: 20, term: 20, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, lterm: 20, term: 20, commit: 0, pterm: 20, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, lterm: 20, term: 20, commit: 0, pterm: 20, pindex: 2, entries: entries})
aeReply := "$TEST"
aeMsg1Fork := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries, reply: aeReply})
aeMsg2Fork := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
// Trigger a catchup.
n.processAppendEntry(aeMsg2, n.aesub)
validateCatchup := func() {
t.Helper()
require_True(t, n.catchup != nil)
require_Equal(t, n.catchup.cterm, 20)
require_Equal(t, n.catchup.cindex, 1)
}
validateCatchup()
// Catchup the first missed entry.
csub := n.catchup.sub
n.processAppendEntry(aeMsg1, csub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.pterm, 20)
nc, err := nats.Connect(n.s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
require_NoError(t, err)
defer nc.Close()
sub, err := nc.SubscribeSync(aeReply)
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
// Would previously stall the catchup and restart it with a previous leader.
n.catchup.pindex = aeMsg1.pindex + 1
n.catchup.active = time.Time{}
n.processAppendEntry(aeMsg1Fork, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.pterm, 20)
validateCatchup()
// Should reply we have a higher term, prompting the server to step down.
msg, err := sub.NextMsg(time.Second)
require_NoError(t, err)
ar := decodeAppendEntryResponse(msg.Data)
require_False(t, ar.success)
require_Equal(t, ar.index, 1)
require_Equal(t, ar.term, 20)
// Would previously reset the WAL.
n.processAppendEntry(aeMsg2Fork, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.pterm, 20)
validateCatchup()
// Now the catchup should continue, undisturbed by an old leader sending append entries.
n.processAppendEntry(aeMsg2, csub)
require_Equal(t, n.pindex, 2)
require_Equal(t, n.pterm, 20)
require_True(t, n.catchup == nil)
// A remaining catchup entry can still be ingested, even if the catchup state itself is gone.
n.processAppendEntry(aeMsg3, csub)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.pterm, 20)
}
func TestNRGProcessed(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 3, pterm: 1, pindex: 2, entries: entries})
aeMsg4 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 4, pterm: 1, pindex: 3, entries: entries})
aeMsg5 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 5, pterm: 1, pindex: 4, entries: entries})
// Store three entries.
for i, aeMsg := range []*appendEntry{aeMsg1, aeMsg2, aeMsg3} {
n.processAppendEntry(aeMsg, n.aesub)
require_Equal(t, n.pindex, uint64(i+1))
require_Equal(t, n.commit, uint64(i+1))
require_Equal(t, n.processed, 0)
require_Equal(t, n.applied, 0)
}
// n.Processed can move both n.processed and n.applied up, with different values.
n.Processed(1, 0)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 3)
require_Equal(t, n.processed, 1)
require_Equal(t, n.applied, 0)
n.Processed(2, 1)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 3)
require_Equal(t, n.processed, 2)
require_Equal(t, n.applied, 1)
// n.Applied moves both n.processed and n.applied up to the same value.
n.Applied(3)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 3)
require_Equal(t, n.processed, 3)
require_Equal(t, n.applied, 3)
// Store the remaining messages.
for i, aeMsg := range []*appendEntry{aeMsg4, aeMsg5} {
n.processAppendEntry(aeMsg, n.aesub)
require_Equal(t, n.pindex, uint64(i+4))
require_Equal(t, n.commit, uint64(i+4))
require_Equal(t, n.processed, 3)
require_Equal(t, n.applied, 3)
}
// An invalid processed call with a higher applied should be clamped back down to the processed index.
n.Processed(4, 5)
require_Equal(t, n.pindex, 5)
require_Equal(t, n.commit, 5)
require_Equal(t, n.processed, 4)
require_Equal(t, n.applied, 4)
}
func TestNRGSendAppendEntryNotLeader(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
require_Equal(t, n.State(), Follower)
require_Equal(t, n.pindex, 0)
nc, err := nats.Connect(n.s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
require_NoError(t, err)
defer nc.Close()
sub, err := nc.SubscribeSync(n.asubj)
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
// sendAppendEntry acquires the lock by itself, so it must also protect against us not being leader anymore.
n.sendAppendEntry([]*Entry{newEntry(EntryNormal, nil)})
// We're a follower, so we should not be able to send or store a message.
require_Equal(t, n.pindex, 0)
msg, err := sub.NextMsg(250 * time.Millisecond)
require_Error(t, err, nats.ErrTimeout)
require_True(t, msg == nil)
}
func TestNRGDrainAndReplaySnapshot(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 2, entries: entries})
aeHeartbeat1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 3, pterm: 1, pindex: 3, entries: nil})
aeMsg4 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 3, pterm: 1, pindex: 3, entries: entries})
aeHeartbeat2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 4, pterm: 1, pindex: 4, entries: nil})
// Stage some entries as normal.
require_Len(t, n.apply.len(), 0)
n.processAppendEntry(aeMsg1, n.aesub)
n.processAppendEntry(aeMsg2, n.aesub)
n.processAppendEntry(aeMsg3, n.aesub)
n.processAppendEntry(aeHeartbeat1, n.aesub)
require_Equal(t, n.pindex, 3)
require_Len(t, n.apply.len(), 3)
require_Equal(t, n.commit, 3)
require_Equal(t, n.hcommit, 0)
// Just a sanity-check, if we have no snapshot then this should fail.
require_False(t, n.DrainAndReplaySnapshot())
require_Len(t, n.apply.len(), 3)
require_Equal(t, n.commit, 3)
require_Equal(t, n.hcommit, 0)
// Simulate this server processing a snapshot that requires upper layer catchup.
// This catchup timed out and we would then call into DrainAndReplaySnapshot.
snap := []byte("snapshot")
n.Applied(1)
require_NoError(t, n.InstallSnapshot(snap, false))
require_True(t, n.DrainAndReplaySnapshot())
require_True(t, n.paused)
require_Len(t, n.apply.len(), 1)
require_Equal(t, n.commit, 1)
require_Equal(t, n.hcommit, 3)
// Simulate snapshot processing being successful and restoring the apply queue when resuming.
n.ResumeApply()
require_False(t, n.paused)
require_Len(t, n.apply.len(), 3)
require_Equal(t, n.commit, 3)
require_Equal(t, n.hcommit, 0)
// Now simulate another case where the snapshot processing times out multiple times.
require_True(t, n.DrainAndReplaySnapshot())
require_True(t, n.paused)
require_Len(t, n.apply.len(), 1)
require_Equal(t, n.commit, 1)
require_Equal(t, n.hcommit, 3)
// Could receive new messages in the meantime and need to keep tracking the highest known commit properly.
n.processAppendEntry(aeMsg4, n.aesub)
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_Equal(t, n.pindex, 4)
require_True(t, n.paused)
require_Len(t, n.apply.len(), 1)
require_Equal(t, n.commit, 1)
require_Equal(t, n.hcommit, 4)
// Replaying again should preserve the highest known commit.
require_True(t, n.DrainAndReplaySnapshot())
require_True(t, n.paused)
require_Len(t, n.apply.len(), 1)
require_Equal(t, n.commit, 1)
require_Equal(t, n.hcommit, 4)
// Resume applies, and ensure correct state.
n.ResumeApply()
require_False(t, n.paused)
require_Len(t, n.apply.len(), 4)
require_Equal(t, n.commit, 4)
require_Equal(t, n.hcommit, 0)
}
func TestNRGTrackPeerActive(t *testing.T) {
// The leader should track timestamps for all peers.
// Each follower should only track the leader, otherwise we would get outdated timestamps.
checkLastSeen := func(peers map[string]RaftzGroupPeer) {
for _, peer := range peers {
if peer.LastSeen == _EMPTY_ {
continue
}
elapsed, err := time.ParseDuration(peer.LastSeen)
require_NoError(t, err)
require_LessThan(t, elapsed, time.Second)
}
}
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
ml := c.leader()
rs := c.randomNonLeader()
var preferred *Server
for _, s := range c.servers {
if s == ml || s == rs {
continue
}
preferred = s
break
}
require_NotNil(t, preferred)
time.Sleep(2 * time.Second)
before := (*rs.Raftz(&RaftzOptions{}))[DEFAULT_SYSTEM_ACCOUNT][defaultMetaGroupName].Peers
checkLastSeen(before)
js := ml.getJetStream()
n := js.getMetaGroup()
require_NoError(t, n.StepDown(preferred.NodeName()))
time.Sleep(2 * time.Second)
after := (*rs.Raftz(&RaftzOptions{}))[DEFAULT_SYSTEM_ACCOUNT][defaultMetaGroupName].Peers
checkLastSeen(after)
}
func TestNRGLostQuorum(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats0 := "S1Nunr6R" // "nats-0"
nats1 := "yrzKKRBu" // "nats-1"
n.Lock()
n.addPeer(nats1)
// Ignore scale-up grace period so this
// test focuses on lost-quorum tracking.
n.lsut = time.Time{}
n.Unlock()
require_Equal(t, n.State(), Follower)
require_False(t, n.Quorum())
require_True(t, n.lostQuorum())
nc, err := nats.Connect(n.s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
require_NoError(t, err)
defer nc.Close()
// Respond to a vote request.
sub, err := nc.Subscribe(n.vsubj, func(m *nats.Msg) {
req := decodeVoteRequest(m.Data, m.Reply)
resp := voteResponse{term: req.term, peer: nats0, granted: true}
m.Respond(resp.encode())
})
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
// Switch to candidate and make sure we properly track the peer as active.
n.switchToCandidate()
require_Equal(t, n.State(), Candidate)
require_False(t, n.Quorum())
require_True(t, n.lostQuorum())
n.Lock()
n.addPeer(nats0)
n.Unlock()
n.runAsCandidate()
require_Equal(t, n.State(), Leader)
require_True(t, n.Quorum())
require_False(t, n.lostQuorum())
}
func TestNRGParallelCatchupRollback(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
aeReply := "$TEST"
nc, err := nats.Connect(n.s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
require_NoError(t, err)
defer nc.Close()
sub, err := nc.SubscribeSync(aeReply)
require_NoError(t, err)
defer sub.Drain()
require_NoError(t, nc.Flush())
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, lterm: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, lterm: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 2, entries: nil, reply: aeReply})
// Trigger a catchup.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 0)
require_NotNil(t, n.catchup)
require_Equal(t, n.catchup.cterm, aeMsg2.pterm)
require_Equal(t, n.catchup.cindex, aeMsg2.pindex)
csub := n.catchup.sub
// Receive the missed messages.
n.processAppendEntry(aeMsg1, csub)
require_Equal(t, n.pindex, 1)
n.processAppendEntry(aeMsg2, csub)
require_Equal(t, n.pindex, 2)
require_True(t, n.catchup == nil)
// Should respond to the heartbeat and allow the leader to commit.
n.processAppendEntry(aeHeartbeat, n.aesub)
msg, err := sub.NextMsg(time.Second)
require_NoError(t, err)
ar := decodeAppendEntryResponse(msg.Data)
require_NotNil(t, ar)
require_True(t, ar.success)
require_Equal(t, ar.term, aeHeartbeat.term)
require_Equal(t, ar.index, aeHeartbeat.pindex)
// Now replay a message that was already received as a catchup entry.
// Likely due to running multiple catchups in parallel.
// Since our WAL is already ahead, we should not truncate based on this.
n.processAppendEntry(aeMsg1, csub)
require_Equal(t, n.pindex, 2)
}
func TestNRGReportLeaderAfterNoopEntry(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
require_Equal(t, n.State(), Follower)
require_Equal(t, n.term, 0)
require_False(t, n.Leader())
n.switchToCandidate()
require_Equal(t, n.State(), Candidate)
require_Equal(t, n.term, 1)
require_False(t, n.Leader())
// Switching to leader will put us into Leader state,
// but we're not necessarily an up-to-date leader yet.
n.switchToLeader()
require_Equal(t, n.State(), Leader)
require_Equal(t, n.term, 1)
require_Equal(t, n.pindex, 1) // Should've sent a NOOP-entry to establish leadership.
require_Equal(t, n.applied, 0)
require_False(t, n.Leader())
// Once we commit and apply the final entry, we should starting to report we're leader.
n.commit = 1
n.Applied(1)
require_Equal(t, n.applied, 1)
require_True(t, n.Leader())
}
func TestNRGSendSnapshotInstallsSnapshot(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats0 := "S1Nunr6R" // "nats-0"
nats1 := "yrzKKRBu" // "nats-1"
n.Lock()
n.addPeer(nats0)
n.addPeer(nats1)
n.Unlock()
require_Equal(t, n.pindex, 0)
require_Equal(t, n.snapfile, _EMPTY_)
// Switch to candidate, to become leader.
require_Equal(t, n.term, 0)
n.switchToCandidate()
require_Equal(t, n.term, 1)
// When switching to leader a NOOP-entry is sent.
n.switchToLeader()
require_Equal(t, n.pindex, 1)
require_Equal(t, n.snapfile, _EMPTY_)
require_NoError(t, n.applyCommit(1))
require_Equal(t, n.snapfile, _EMPTY_)
// On scaleup, we send a snapshot.
require_NoError(t, n.SendSnapshot([]byte("snapshot_data")))
require_Equal(t, n.pindex, 2)
require_Equal(t, n.snapfile, _EMPTY_)
// When applying the entry, the sent snapshot should be installed.
require_NoError(t, n.applyCommit(2))
require_NotEqual(t, n.snapfile, _EMPTY_)
snap, err := n.loadLastSnapshot()
require_NoError(t, err)
require_NotNil(t, snap)
require_Equal(t, snap.lastTerm, 1)
require_Equal(t, snap.lastIndex, 1)
require_Equal(t, string(snap.data), "snapshot_data")
// Draining and replaying the snapshot should work.
require_True(t, n.DrainAndReplaySnapshot())
}
func TestNRGQuorumAfterLeaderStepdown(t *testing.T) {
origMinTimeout, origMaxTimeout, origHBInterval := minElectionTimeout, maxElectionTimeout, hbInterval
minElectionTimeout, maxElectionTimeout, hbInterval = minElectionTimeoutDefault, maxElectionTimeoutDefault, hbIntervalDefault
defer func() {
minElectionTimeout, maxElectionTimeout, hbInterval = origMinTimeout, origMaxTimeout, origHBInterval
}()
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats0 := "S1Nunr6R" // "nats-0"
nats1 := "yrzKKRBu" // "nats-1"
n.Lock()
n.addPeer(nats0)
n.addPeer(nats1)
n.Unlock()
// Become leader.
n.switchToCandidate()
n.switchToLeader()
// Should not report having quorum, unless at least 1 peer is seen.
require_False(t, n.Quorum())
n.s.nodeToInfo.Store(nats0, nodeInfo{})
require_NoError(t, n.trackPeer(nats0))
require_True(t, n.Quorum())
n.s.nodeToInfo.Store(nats1, nodeInfo{})
require_NoError(t, n.trackPeer(nats1))
require_True(t, n.Quorum())
require_Len(t, len(n.peers), 3)
for _, ps := range n.peers {
ps.kp = true
}
// If we hand off leadership to another server, we should
// still be reporting we have quorum.
require_NoError(t, n.StepDown())
require_True(t, n.Quorum())
require_Len(t, len(n.peers), 3)
for peer, ps := range n.peers {
if peer == n.id {
continue
}
// All peer timestamps should be preserved.
require_False(t, ps.ts.IsZero())
}
// After our new leader comes online, we should still have quorum,
// but the other follower's timestamp should be cleared.
n.updateLeader(nats0)
require_Equal(t, n.leader, nats0)
require_True(t, n.Quorum())
require_Len(t, len(n.peers), 3)
for peer, ps := range n.peers {
if peer == n.id {
continue
}
if peer == nats0 {
require_False(t, ps.ts.IsZero()) // Leader is preserved.
} else {
require_True(t, ps.ts.IsZero()) // Other follower is cleared.
}
}
}
func TestNRGNoLogResetOnCorruptedSendToFollower(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 2)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 2, newStateAdder)
rg.waitOnLeader()
leader := rg.leader().(*stateAdder)
follower := rg.nonLeader().(*stateAdder)
leaderrg := leader.node().(*raft)
followerrg := follower.node().(*raft)
for i := range int64(10) {
leader.proposeDelta(i + 1)
time.Sleep(50 * time.Millisecond) // ... for multiple AEs.
}
// Snapshot and compact.
rg.waitOnTotal(t, 55)
leader.snapshot(t)
// Above snapshot should have resulted in compaction of the WAL.
var ss StreamState
leaderrg.wal.FastState(&ss)
require_Equal(t, ss.Msgs, 0)
// Stop the follower, we'll flatten their state so they have to
// request a snapshot.
require_NoError(t, followerrg.wal.Truncate(0))
follower.stop()
// Now we're going to subtly corrupt the snapshot on the leader.
stat, err := os.Stat(leaderrg.snapfile)
require_NoError(t, err)
require_NoError(t, os.Truncate(leaderrg.snapfile, stat.Size()-1))
// Now we'll bring the follower back. It should request a snapshot
// from the leader. Previously this would have caused the leader to
// blow away the entire log, but in reality we will probably just
// install a new snapshot at some point soon anyway.
follower.restart()
c.waitOnAllCurrent()
leaderrg.wal.FastState(&ss)
require_NotEqual(t, ss.LastSeq, 0)
}
func TestNRGTruncateLogWithMisalignedSnapshotGap(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
s := c.servers[0] // RunBasicJetStreamServer not available
defer c.shutdown()
storeDir := t.TempDir()
fcfg := FileStoreConfig{StoreDir: storeDir, BlockSize: defaultMediumBlockSize, AsyncFlush: false, srv: s}
scfg := StreamConfig{Name: "RAFT", Storage: FileStorage}
fs, err := newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg := &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
err = s.bootstrapRaftNode(cfg, nil, false)
require_NoError(t, err)
n, err := s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: entries})
for i, ae := range []*appendEntry{aeMsg1, aeMsg2, aeMsg3} {
n.processAppendEntry(ae, n.aesub)
require_Equal(t, n.pindex, uint64(i+1))
}
// Manually call back down to applied, and then snapshot.
n.Applied(1)
require_NoError(t, n.InstallSnapshot(nil, false))
state := n.wal.State()
require_Equal(t, state.FirstSeq, 2)
require_Equal(t, state.LastSeq, 3)
// Manually compact the WAL further so it doesn't align anymore with the snapshot.
_, err = n.wal.Compact(3)
require_NoError(t, err)
state = n.wal.State()
require_Equal(t, state.FirstSeq, 3)
require_Equal(t, state.LastSeq, 3)
// Restart.
n.Stop()
n.WaitForStop()
require_NoError(t, fs.Stop())
fs, err = newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg = &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
n, err = s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// The gap between the snapshot and the WAL should be detected, and the WAL should truncate.
// Can't continue normally with missing entries.
state = n.wal.State()
require_Equal(t, state.Msgs, 0)
require_Equal(t, state.FirstSeq, 2)
require_Equal(t, state.LastSeq, 1)
require_Equal(t, n.pindex, 1)
// Should be able to re-populate the missing messages.
// Need to re-encode them, though, since they would have been returned to the pool before.
aeMsg2 = encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
aeMsg3 = encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: entries})
for i, ae := range []*appendEntry{aeMsg2, aeMsg3} {
n.processAppendEntry(ae, n.aesub)
require_Equal(t, n.pindex, uint64(i+2))
}
}
func TestNRGTruncateLogWithMissingSnapshot(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
s := c.servers[0] // RunBasicJetStreamServer not available
defer c.shutdown()
storeDir := t.TempDir()
fcfg := FileStoreConfig{StoreDir: storeDir, BlockSize: defaultMediumBlockSize, AsyncFlush: false, srv: s}
scfg := StreamConfig{Name: "RAFT", Storage: FileStorage}
fs, err := newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg := &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
err = s.bootstrapRaftNode(cfg, nil, false)
require_NoError(t, err)
n, err := s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: entries})
for i, ae := range []*appendEntry{aeMsg1, aeMsg2, aeMsg3} {
n.processAppendEntry(ae, n.aesub)
require_Equal(t, n.pindex, uint64(i+1))
}
// Manually simulate a snapshot being made, only compacting but not actually installing a snapshot.
_, err = n.wal.Compact(2)
require_NoError(t, err)
state := n.wal.State()
require_Equal(t, state.FirstSeq, 2)
require_Equal(t, state.LastSeq, 3)
// Restart.
n.Stop()
n.WaitForStop()
require_NoError(t, fs.Stop())
fs, err = newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg = &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
n, err = s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// The gap between the snapshot and the WAL should be detected, and the WAL should truncate.
// Can't continue normally with missing entries.
state = n.wal.State()
require_Equal(t, state.Msgs, 0)
require_Equal(t, state.FirstSeq, 0)
require_Equal(t, state.LastSeq, 0)
require_Equal(t, n.pindex, 0)
}
// This is a RaftChainOfBlocks test where a block is proposed and then we wait for all replicas to apply it before
// proposing the next one.
// The test may fail if:
// - Replicas hash diverge
// - One replica never applies the N-th block applied by the rest
// - The given number of blocks cannot be applied within some amount of time
func TestNRGChainOfBlocksRunInLockstep(t *testing.T) {
const iterations = 50
const applyTimeout = 3 * time.Second
const testTimeout = iterations * time.Second
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newRaftChainStateMachine)
rg.waitOnLeader()
testTimer := time.NewTimer(testTimeout)
for iteration := uint64(1); iteration <= iterations; iteration++ {
select {
case <-testTimer.C:
t.Fatalf("Timeout, completed %d/%d iterations", iteration-1, iterations)
default:
// Continue
}
// Propose the next block (this test assumes the proposal always goes through)
rg.randomMember().(*RCOBStateMachine).proposeBlock()
var currentHash string
// Wait on participants to converge
checkFor(t, applyTimeout, 500*time.Millisecond, func() error {
var previousNodeName string
var previousNodeHash string
for _, sm := range rg {
stateMachine := sm.(*RCOBStateMachine)
nodeName := fmt.Sprintf(
"%s/%s",
stateMachine.server().Name(),
stateMachine.node().ID(),
)
running, blocksCount, hash := stateMachine.getCurrentHash()
// All nodes always running
if !running {
return fmt.Errorf(
"node %s is not running",
nodeName,
)
}
// Node is behind
if blocksCount != iteration {
return fmt.Errorf(
"node %s applied %d blocks out of %d expected",
nodeName,
blocksCount,
iteration,
)
}
// Make sure hash is not empty
if hash == "" {
return fmt.Errorf(
"node %s has empty hash after applying %d blocks",
nodeName,
blocksCount,
)
}
// Check against previous node hash, unless this is the first node and we don't have anyone to compare
if previousNodeHash != "" && previousNodeHash != hash {
return fmt.Errorf(
"hash mismatch after %d blocks: %s hash: %s != %s hash: %s",
iteration,
nodeName,
hash,
previousNodeName,
previousNodeHash,
)
}
// Set node name and hash for next node to compare against
previousNodeName, previousNodeHash = nodeName, hash
}
// All replicas applied the last block and their hashes match
currentHash = previousNodeHash
return nil
})
if RCOBOptions.verbose {
t.Logf(
"Verified chain hash %s for %d/%d nodes after %d/%d iterations",
currentHash,
len(rg),
len(rg),
iteration,
iterations,
)
}
}
}
// This is a RaftChainOfBlocks test where one of the replicas is stopped before proposing a short burst of blocks.
// Upon resuming the replica, we check it is able to catch up to the rest.
// The test may fail if:
// - Replicas hash diverge
// - One replica never applies the N-th block applied by the rest
// - The given number of blocks cannot be applied within some amount of time
func TestNRGChainOfBlocksStopAndCatchUp(t *testing.T) {
const iterations = 50
const blocksPerIteration = 3
const applyTimeout = 3 * time.Second
const testTimeout = 2 * iterations * time.Second
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createRaftGroup("TEST", 3, newRaftChainStateMachine)
rg.waitOnLeader()
testTimer := time.NewTimer(testTimeout)
highestBlockSeen := uint64(0)
for iteration := uint64(1); iteration <= iterations; iteration++ {
select {
case <-testTimer.C:
t.Fatalf("Timeout, completed %d/%d iterations", iteration-1, iterations)
default:
// Continue
}
// Stop a random node
stoppedNode := rg.randomMember()
leader := stoppedNode.node().Leader()
stoppedNode.stop()
// Snapshot a random (non-stopped) node
snapshotNode := rg.randomMember()
for snapshotNode == stoppedNode {
snapshotNode = rg.randomMember()
}
snapshotNode.(*RCOBStateMachine).createSnapshot()
if RCOBOptions.verbose {
t.Logf(
"Iteration %d/%d: stopping node: %s/%s (leader: %v)",
iteration,
iterations,
stoppedNode.server().Name(),
stoppedNode.node().ID(),
leader,
)
}
// Propose some new blocks
rg.waitOnLeader()
for i := 0; i < blocksPerIteration; i++ {
proposer := rg.randomMember()
// Pick again if we randomly chose the stopped node
for proposer == stoppedNode {
proposer = rg.randomMember()
}
proposer.(*RCOBStateMachine).proposeBlock()
}
// Restart the stopped node
stoppedNode.restart()
// Wait on participants to converge
expectedBlocks := iteration * blocksPerIteration
var currentHash string
checkFor(t, applyTimeout, 250*time.Millisecond, func() error {
var previousNodeName string
var previousNodeHash string
for _, sm := range rg {
stateMachine := sm.(*RCOBStateMachine)
nodeName := fmt.Sprintf(
"%s/%s",
stateMachine.server().Name(),
stateMachine.node().ID(),
)
running, blocksCount, currentHash := stateMachine.getCurrentHash()
// Track the highest block seen by any replica
if blocksCount > highestBlockSeen {
highestBlockSeen = blocksCount
// Must check all replicas again
return fmt.Errorf("updated highest block to %d (%s)", highestBlockSeen, nodeName)
}
// All nodes should be running
if !running {
return fmt.Errorf(
"node %s not running",
nodeName,
)
}
// Node is behind
if blocksCount != expectedBlocks {
return fmt.Errorf(
"node %s applied %d blocks out of %d expected",
nodeName,
blocksCount,
expectedBlocks,
)
}
// Make sure hash is not empty
if currentHash == "" {
return fmt.Errorf(
"node %s has empty hash after applying %d blocks",
nodeName,
blocksCount,
)
}
// Check against previous node hash, unless this is the first node to be checked
if previousNodeHash != "" && previousNodeHash != currentHash {
return fmt.Errorf(
"hash mismatch after %d blocks: %s hash: %s != %s hash: %s",
expectedBlocks,
nodeName,
currentHash,
previousNodeName,
previousNodeHash,
)
}
// Set node name and hash for next node to compare against
previousNodeName, previousNodeHash = nodeName, currentHash
}
// All is well
currentHash = previousNodeHash
return nil
})
if RCOBOptions.verbose {
t.Logf(
"Verified chain hash %s for %d/%d nodes after %d blocks, %d/%d iterations (%d lost proposals)",
currentHash,
len(rg),
len(rg),
highestBlockSeen,
iteration,
iterations,
expectedBlocks-highestBlockSeen,
)
}
}
}
func TestNRGProposeRemovePeer(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
n := rg.leader().node()
rg.waitOnLeader()
peerId := rg.nonLeader().node().ID()
require_NoError(t, n.ProposeRemovePeer(peerId))
checkFor(t, 10*time.Second, 200*time.Millisecond, func() error {
var err error
for _, r := range rg {
if len(r.node().Peers()) != 2 {
err = errors.New("has not removed peer")
break
}
}
return err
})
}
func TestNRGProposeRemovePeerNonExistent(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
leader := rg.leader().node().(*raft)
const bogus = "deadbeef" // 8 chars, matches idLen
leader.RLock()
_, exists := leader.peers[bogus]
leader.RUnlock()
require_False(t, exists)
// Removing a peer not in the group should fail synchronously and have no side effects.
require_Error(t, leader.ProposeRemovePeer(bogus), errPeerNotFound)
require_False(t, leader.MembershipChangeInProgress())
for _, r := range rg {
n := r.node().(*raft)
n.RLock()
_, removed := n.removed[bogus]
n.RUnlock()
require_False(t, removed)
require_Equal(t, len(r.node().Peers()), 3)
}
// A real removal still works afterward.
realPeer := rg.nonLeader().node().ID()
require_NoError(t, leader.ProposeRemovePeer(realPeer))
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
for _, r := range rg {
if len(r.node().Peers()) != 2 {
return errors.New("has not removed peer")
}
}
return nil
})
}
func TestNRGForwardedRemovePeerProposalNonExistent(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
leader := rg.leader().node().(*raft)
follower := rg.nonLeader().node()
const bogus = "deadbeef" // 8 chars, matches idLen
leader.RLock()
_, exists := leader.peers[bogus]
leader.RUnlock()
require_False(t, exists)
// ProposeRemovePeer on a follower forwards to the leader's
// handleForwardedRemovePeerProposal. The follower returns nil regardless,
// so we have to inspect the leader directly for the side effects.
require_NoError(t, follower.ProposeRemovePeer(bogus))
// Give the forwarded RPC plenty of time to arrive and (incorrectly) take
// effect if the existence check were missing. With the check, no
// membership change is queued and the removed map stays clean.
time.Sleep(500 * time.Millisecond)
require_False(t, leader.MembershipChangeInProgress())
for _, r := range rg {
n := r.node().(*raft)
n.RLock()
_, removed := n.removed[bogus]
n.RUnlock()
require_False(t, removed)
require_Equal(t, len(r.node().Peers()), 3)
}
// A real forwarded removal still works.
realPeer := follower.ID()
require_NoError(t, follower.ProposeRemovePeer(realPeer))
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
for _, r := range rg {
if len(r.node().Peers()) != 2 {
return errors.New("has not removed peer")
}
}
return nil
})
}
func TestNRGProposeRemovePeerConcurrent(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
n := rg.leader().node()
locked := rg.lockFollowers()
// Attempt to remove the first follower, should succeed.
err := n.ProposeRemovePeer(locked[0].node().ID())
require_NoError(t, err)
// Check that membership change is in progress.
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
if n.MembershipChangeInProgress() {
return nil
} else {
return errors.New("membership not in progress")
}
})
// Attempt to remove the second follower, should fail.
err = n.ProposeRemovePeer(locked[1].node().ID())
require_Error(t, err, errMembershipChange)
for _, l := range locked {
l.node().(*raft).Unlock()
}
// Expect only one peer removal to succeed
checkFor(t, 10*time.Second, 200*time.Millisecond, func() error {
var err error
for _, r := range rg {
if len(r.node().Peers()) != 2 {
err = errors.New("has not removed peer")
break
}
}
return err
})
}
func TestNRGUncommittedMembershipChangeOnNewLeader(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats1 := "yrzKKRBu" // "nats-1"
nats2 := "cnrtt3eg" // "nats-2"
n.Lock()
n.addPeer(nats1)
n.addPeer(nats2)
n.Unlock()
entries := []*Entry{newEntry(EntryRemovePeer, []byte(nats2))}
aeRemovePeer := encode(t, &appendEntry{leader: nats1, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
// plant a EntryRemovePeer in the log
n.processAppendEntry(aeRemovePeer, n.aesub)
require_Equal(t, n.pindex, 1)
require_False(t, n.Healthy())
// become the new leader
n.term = 2
n.switchToLeader()
err := n.ProposeRemovePeer(nats1)
require_Error(t, err, errMembershipChange)
}
func TestNRGUncommittedMembershipChangeGetsTruncated(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats1 := "yrzKKRBu" // "nats-1"
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
aeMsg := encode(t, &appendEntry{leader: nats1, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
entries = []*Entry{newEntry(EntryAddPeer, []byte(nats1))}
aeAddPeer := encode(t, &appendEntry{leader: nats1, term: 1, commit: 0, pterm: 1, pindex: 1, entries: entries})
// Set up a membership change.
n.processAppendEntry(aeMsg, n.aesub)
n.processAppendEntry(aeAddPeer, n.aesub)
require_Equal(t, n.pindex, 2)
require_True(t, n.MembershipChangeInProgress())
require_Equal(t, n.membChangeIndex, 2)
// If the entry containing the membership change isn't truncated, it should remain in progress.
n.truncateWAL(n.pterm, n.pindex)
require_True(t, n.MembershipChangeInProgress())
require_Equal(t, n.membChangeIndex, 2)
// If the entry IS truncated, then it shouldn't be in progress anymore.
n.truncateWAL(n.pterm, n.pindex-1)
require_False(t, n.MembershipChangeInProgress())
require_Equal(t, n.membChangeIndex, 0)
}
func TestNRGUncommittedMembershipChangeOnNewLeaderForwardedRemovePeerProposal(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats1 := "yrzKKRBu" // "nats-1"
nats2 := "cnrtt3eg" // "nats-2"
n.Lock()
n.addPeer(nats1)
n.addPeer(nats2)
n.Unlock()
entries := []*Entry{newEntry(EntryRemovePeer, []byte(nats2))}
aeRemovePeer := encode(t, &appendEntry{leader: nats1, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
// plant a EntryRemovePeer in the log
n.processAppendEntry(aeRemovePeer, n.aesub)
require_Equal(t, n.pindex, 1)
require_False(t, n.Healthy())
// become the new leader
n.term = 2
n.switchToLeader()
n.leaderState.Store(true)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
n.runAsLeader()
}()
defer wg.Wait()
n.RLock()
rpsubj := n.rpsubj
n.RUnlock()
nc, _ := jsClientConnect(t, n.s, nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
// Forward a peer-remove proposal to the new leader.
before, _, _ := n.Progress()
require_True(t, n.MembershipChangeInProgress())
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
_, err := nc.Request(rpsubj, []byte(nats1), 100*time.Millisecond)
// Wait for the server to be subscribed and either respond or time out.
if err == nil || errors.Is(err, nats.ErrTimeout) {
return nil
}
return err
})
// The forwarded peer-remove should not be accepted.
time.Sleep(200 * time.Millisecond)
after, _, _ := n.Progress()
require_Equal(t, before, after)
require_True(t, n.MembershipChangeInProgress())
}
func TestNRGIgnoreForwardedProposalIfNotCaughtUpLeader(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats1 := "yrzKKRBu" // "nats-1"
nats2 := "cnrtt3eg" // "nats-2"
n.Lock()
n.addPeer(nats1)
n.addPeer(nats2)
n.Unlock()
n.term = 1
n.switchToLeader()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
n.runAsLeader()
}()
defer wg.Wait()
n.RLock()
// Although this server is the leader, mark it as not caught up yet.
n.leaderState.Store(false)
psubj := n.psubj
n.RUnlock()
nc, _ := jsClientConnect(t, n.s, nats.UserInfo("admin", "s3cr3t!"))
defer nc.Close()
// Forward a normal proposal to the new leader.
before, _, _ := n.Progress()
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
_, err := nc.Request(psubj, nil, 100*time.Millisecond)
// Wait for the server to be subscribed and either respond or time out.
if err == nil || errors.Is(err, nats.ErrTimeout) {
return nil
}
return err
})
// The forwarded proposal should not be accepted.
time.Sleep(200 * time.Millisecond)
after, _, _ := n.Progress()
require_Equal(t, before, after)
}
func TestNRGProposeRemovePeerQuorum(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
leader := rg.leader().node()
followers := rg.followers()
require_True(t, len(followers) == 2)
// Block follower 0 and remove follower 1
followers[0].node().(*raft).Lock()
err := leader.ProposeRemovePeer(followers[1].node().ID())
require_NoError(t, err)
// Should not be able to make progress
time.Sleep(time.Second)
require_True(t, leader.MembershipChangeInProgress())
// Unlock the other follower and expect the membership
// change to eventually finish
followers[0].node().(*raft).Unlock()
checkFor(t, 10*time.Second, 200*time.Millisecond, func() error {
if leader.MembershipChangeInProgress() {
return errors.New("membership still in progress")
} else {
return nil
}
})
}
// Test outline:
// - In a R3 cluster, PeerRemove the leader and block on of
// the followers.
// - Verify that the membership change can't make progress,
// the leader should not count its own ack towards quorum.
// - Release the previously blocked follower and expect the
// membership change to take place.
func TestNRGProposeRemovePeerLeader(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
leader := rg.leader().node()
followers := rg.followers()
leaderID := leader.ID()
require_True(t, len(followers) == 2)
// Block follower 0 and remove the leader
followers[0].node().(*raft).Lock()
err := leader.ProposeRemovePeer(leader.ID())
require_NoError(t, err)
// Should not be able to make progress
time.Sleep(time.Second)
require_True(t, leader.MembershipChangeInProgress())
// Unlock the follower and expect the membership
// change to eventually finish
followers[0].node().(*raft).Unlock()
checkFor(t, 10*time.Second, 200*time.Millisecond, func() error {
if leader.MembershipChangeInProgress() {
return errors.New("membership still in progress")
} else {
return nil
}
})
// Old leader steps down
checkFor(t, 10*time.Second, 200*time.Millisecond, func() error {
newLeader := rg.waitOnLeader()
if newLeader.node().ID() == leaderID {
return errors.New("leader has not changed yet")
}
return nil
})
newLeader := rg.waitOnLeader()
require_Equal(t, leader.State(), Closed)
require_NotEqual(t, leader.ID(), newLeader.node().ID())
require_Equal(t, len(newLeader.node().Peers()), 2)
require_False(t, newLeader.node().MembershipChangeInProgress())
}
func TestNRGProposeRemovePeerAll(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
leader := rg.leader()
followers := rg.followers()
require_Equal(t, len(followers), 2)
peers := leader.node().Peers()
require_Equal(t, len(peers), 3)
for i, follower := range followers {
require_NoError(t, leader.node().ProposeRemovePeer(follower.node().ID()))
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
if peers = leader.node().Peers(); len(peers) == 2-i {
return nil
}
return errors.New("membership still in progress")
})
}
peers = leader.node().Peers()
leaderID := leader.node().ID()
// The leader is the only one left...
require_Equal(t, len(peers), 1)
require_Equal(t, peers[0].ID, leaderID)
// and we can't remove it
require_Error(t, leader.node().ProposeRemovePeer(leaderID), errRemoveLastNode)
}
func TestNRGLeaderResurrectsRemovedPeers(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
leader := rg.leader()
followers := rg.followers()
require_Equal(t, len(followers), 2)
// Remove one follower
require_NoError(t, leader.node().ProposeRemovePeer(followers[0].node().ID()))
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
if peers := leader.node().Peers(); len(peers) == 2 {
return nil
}
return errors.New("membership still in progress")
})
// Stop the leader and restart it.
// If bug is present: the leader resurrects the previously removed peer.
leader.stop()
followers[1].stop()
leader.restart()
require_Equal(t, len(leader.node().Peers()), 2)
followers[1].restart()
require_Equal(t, len(leader.node().Peers()), 2)
}
func TestNRGAddPeers(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
leader := rg.waitOnLeader()
require_Equal(t, leader.node().ClusterSize(), 3)
for range 6 {
rg = append(rg, c.addMemRaftNode("TEST", newStateAdder))
}
checkFor(t, 1*time.Second, 10*time.Millisecond, func() error {
if leader.node().ClusterSize() != 9 {
return errors.New("node additions still in progress")
}
return nil
})
require_Equal(t, leader.node().ClusterSize(), 9)
}
func TestNRGDisjointMajorities(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
rg.waitOnLeader()
leader := rg.leader()
followers := rg.followers()
require_Equal(t, len(followers), 2)
// Lock all followers, a majority.
locked := rg.lockFollowers()
require_Equal(t, len(locked), 2)
defer func() {
for _, l := range locked {
l.node().(*raft).Unlock()
}
}()
// Add one node (cluster size is 4)
c.addMemRaftNode("TEST", newStateAdder)
checkFor(t, 1*time.Second, 10*time.Millisecond, func() error {
if leader.node().ClusterSize() != 4 {
return errors.New("node addition still in progress")
}
return nil
})
// Attempt another node addition
c.addMemRaftNode("TEST", newStateAdder)
// If bug is present:
// The leader is able to form a majority because it would
// add peers immediately, and readjust cluster size and
// quorum only after committing EntryAddPeer. This allowed
// the leader to commit the entries using only newly added
// nodes (the original followers are locked and not
// acknowledging the EntryAddPeer proposals!)
// This should not never happen. In a real scenario the
// followers could be partitioned, and they actually have
// a majority... so they could diverge and we end up with
// two different histories.
//
// Here wait a little bit, and check that the leader is
// unable to make any progess.
time.Sleep(time.Second)
require_Equal(t, leader.node().ClusterSize(), 4)
require_Equal(t, leader.node().MembershipChangeInProgress(), true)
}
func TestNRGAppendEntryResurrectsLeader(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
S2 := "z3WIzPtj" // S-2
n.addPeer(S2)
require_Equal(t, len(n.peers), 2)
require_Equal(t, n.ClusterSize(), 2)
// PeerRemove S2
entries := []*Entry{newEntry(EntryRemovePeer, []byte(S2))}
aeRemovePeer := encode(t, &appendEntry{
leader: S2, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
n.processAppendEntry(aeRemovePeer, n.aesub)
// Heartbeat commits the PeerRemove
aeHeartBeat := encode(t, &appendEntry{
leader: S2, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
n.processAppendEntry(aeHeartBeat, n.aesub)
require_Equal(t, len(n.peers), 1)
require_Equal(t, n.ClusterSize(), 1)
// If bug is present: receiving a appendEntry from the old leader
// will resurrect it. In practice, this has been observed with
// LeaderTransfer entries, but any type of entry will do...
aeHeartBeat2 := encode(t, &appendEntry{
leader: S2, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
n.processAppendEntry(aeHeartBeat2, n.aesub)
// Expect the cluster size to be unchanged
require_Equal(t, len(n.peers), 1)
require_Equal(t, n.ClusterSize(), 1)
}
func TestNRGSingleNodeElection(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
rg := c.createMemRaftGroup("TEST", 3, newStateAdder)
// Remove the cluster leader, and then again
for range 2 {
leader := rg.waitOnLeader().node()
require_NoError(t, leader.ProposeRemovePeer(leader.ID()))
checkFor(t, 1*time.Second, 10*time.Millisecond, func() error {
if leader.State() == Leader {
return errors.New("Removed node is still leader")
}
return nil
})
require_False(t, leader.MembershipChangeInProgress())
}
// The remaining follower must be able to become leader
// on its own
newLeader := rg.waitOnLeader()
require_Equal(t, len(newLeader.node().Peers()), 1)
require_Equal(t, newLeader.node().ClusterSize(), 1)
require_False(t, newLeader.node().MembershipChangeInProgress())
adder := newLeader.(*stateAdder)
adder.proposeDelta(1)
adder.proposeDelta(10)
adder.proposeDelta(100)
rg.waitOnTotal(t, 111)
// Add two nodes back
rg = append(rg, c.addMemRaftNode("TEST", newStateAdder))
rg = append(rg, c.addMemRaftNode("TEST", newStateAdder))
checkFor(t, 1*time.Second, 10*time.Millisecond, func() error {
if newLeader.node().ClusterSize() != 3 {
return errors.New("node additions still in progress")
}
return nil
})
checkFor(t, 1*time.Second, 10*time.Millisecond, func() error {
if newLeader.node().MembershipChangeInProgress() {
return errors.New("membership still in progress")
}
return nil
})
rg.waitOnTotal(t, 111)
require_Equal(t, newLeader.node().ClusterSize(), 3)
require_False(t, newLeader.node().MembershipChangeInProgress())
}
func TestNRGMustNotResetVoteOnStepDownOrLeaderTransfer(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
nats0 := "S1Nunr6R" // "nats-0"
// Vote for a new leader.
require_NoError(t, n.processVoteRequest(&voteRequest{term: 1, candidate: nats0}))
require_Equal(t, n.term, 1)
require_Equal(t, n.vote, nats0)
// Stepping down as leader must NOT reset the vote info.
n.state.Store(int32(Leader))
require_NoError(t, n.StepDown())
require_Equal(t, n.vote, nats0)
// A leader transfer must NOT reset the vote info.
// This is automatically cleared once the intended leader starts a new election with a higher term.
entries := []*Entry{newEntry(EntryLeaderTransfer, []byte(nats0))}
aeLeaderTransfer := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
n.processAppendEntry(aeLeaderTransfer, n.aesub)
require_Equal(t, n.term, 1)
require_Equal(t, n.vote, nats0)
}
func TestNRGInstallSnapshotFromCheckpoint(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: entries})
aeMsg3 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 2, pterm: 1, pindex: 2, entries: entries})
aeMsg4 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 3, pterm: 1, pindex: 3, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 4, pterm: 1, pindex: 4, entries: nil})
// Process the first set of messages.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 0)
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 2)
require_Equal(t, n.commit, 1)
n.Applied(1)
require_Equal(t, n.applied, 1)
// Start the first checkpoint, creating the first snapshot.
c, err := n.CreateSnapshotCheckpoint(false)
require_NoError(t, err)
// There's no previous snapshot.
buf, err := c.LoadLastSnapshot()
require_Error(t, err, errNoSnapAvailable)
require_True(t, buf == nil)
// Since only 1 entry was applied, we should be able to retrieve that here.
var count int
for ae, err := range c.AppendEntriesSeq() {
count++
require_NoError(t, err)
require_Equal(t, ae.pindex, 0)
}
require_Equal(t, count, 1)
// Install the snapshot and confirm we can retrieve it.
_, err = c.InstallSnapshot([]byte("old"))
require_NoError(t, err)
snap, err := n.loadLastSnapshot()
require_NoError(t, err)
require_True(t, bytes.Equal(snap.data, []byte("old")))
// Checkpoint was installed, so all should now abort.
_, err = c.LoadLastSnapshot()
require_Error(t, err, errSnapAborted)
count = 0
for _, err = range c.AppendEntriesSeq() {
count++
require_Error(t, err, errSnapAborted)
}
require_Equal(t, count, 1)
_, err = c.InstallSnapshot(nil)
require_Error(t, err, errSnapAborted)
// Process the next set of messages, we'll be able to compact two append entries now.
n.processAppendEntry(aeMsg3, n.aesub)
require_Equal(t, n.pindex, 3)
require_Equal(t, n.commit, 2)
n.processAppendEntry(aeMsg4, n.aesub)
require_Equal(t, n.pindex, 4)
require_Equal(t, n.commit, 3)
n.Applied(3)
require_Equal(t, n.applied, 3)
// Check a couple edge cases now, a second checkpoint should fail noting a snapshot is already in progress.
c, err = n.CreateSnapshotCheckpoint(false)
require_NoError(t, err)
_, err = n.CreateSnapshotCheckpoint(false)
require_Error(t, err, errSnapInProgress)
// Abort checkpoint, so all should now abort.
c.Abort()
_, err = c.LoadLastSnapshot()
require_Error(t, err, errSnapAborted)
count = 0
for _, err = range c.AppendEntriesSeq() {
count++
require_Error(t, err, errSnapAborted)
}
require_Equal(t, count, 1) // Must always iterate at least once to retrieve the error.
_, err = c.InstallSnapshot(nil)
require_Error(t, err, errSnapAborted)
// Start the second checkpoint, creating the second snapshot.
c, err = n.CreateSnapshotCheckpoint(false)
require_NoError(t, err)
// Should load the previous snapshot.
buf, err = c.LoadLastSnapshot()
require_NoError(t, err)
require_True(t, bytes.Equal(buf, []byte("old")))
// Iterate over the two append entries part of this new snapshot.
count = 0
for ae, err := range c.AppendEntriesSeq() {
count++
require_NoError(t, err)
require_Equal(t, ae.pindex, uint64(count))
}
require_Equal(t, count, 2)
// Install the snapshot and confirm we can retrieve it.
_, err = c.InstallSnapshot([]byte("new"))
require_NoError(t, err)
snap, err = n.loadLastSnapshot()
require_NoError(t, err)
require_True(t, bytes.Equal(snap.data, []byte("new")))
// Commit and apply the last entry to test for the final edge case.
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.pindex, 4)
require_Equal(t, n.commit, 4)
n.Applied(4)
require_Equal(t, n.applied, 4)
// Start an async snapshot.
c, err = n.CreateSnapshotCheckpoint(false)
require_NoError(t, err)
// Installing a snapshot normally should be denied, as one is already in progress.
require_Error(t, n.InstallSnapshot(nil, false), errSnapInProgress)
// The checkpoint should still work successfully.
buf, err = c.LoadLastSnapshot()
require_NoError(t, err)
require_True(t, bytes.Equal(buf, []byte("new")))
// However, if we install using the internal API, this should be allowed and abort our checkpoint.
// This API will be used as a result of receiving a snapshot as part of catchup, so it's really important
// we aren't allowed to replace this newest snapshot with an older snapshot from an earlier checkpoint.
snap.lastIndex++
require_NoError(t, n.installSnapshot(snap))
// Check it's aborted.
_, err = c.LoadLastSnapshot()
require_Error(t, err, errSnapAborted)
// Check if the last snapshot was updated from underneath of us.
n.Lock()
n.snapshotting = true
n.Unlock()
_, err = c.LoadLastSnapshot()
require_Error(t, err, errors.New("snapshot index mismatch"))
}
func TestNRGInstallSnapshotForce(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeHeartbeat := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
n.processAppendEntry(aeMsg, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 0)
n.processAppendEntry(aeHeartbeat, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 1)
n.Applied(1)
require_Equal(t, n.applied, 1)
// Block snapshots unless they're forced to skip in-progress catchups.
n.progress = make(map[string]*ipQueue[uint64])
n.progress["blockSnapshots"] = newIPQueue[uint64](n.s, "blockSnapshots")
require_Error(t, n.InstallSnapshot(nil, false), errCatchupsRunning)
require_Equal(t, n.papplied, 0)
require_NoError(t, n.InstallSnapshot(nil, true))
require_Equal(t, n.papplied, 1)
}
func TestNRGInstallSnapshotFromCheckpointAfterTruncateToSnapshot(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
// Create a sample entry, the content doesn't matter, just that it's stored.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
entries := []*Entry{newEntry(EntryNormal, esm)}
nats0 := "S1Nunr6R" // "nats-0"
// Timeline
aeMsg1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 0, pterm: 0, pindex: 0, entries: entries})
aeHeartbeat1 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 1, pterm: 1, pindex: 1, entries: nil})
aeMsg2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 100, pterm: 1, pindex: 100, entries: entries})
aeHeartbeat2 := encode(t, &appendEntry{leader: nats0, term: 1, commit: 101, pterm: 1, pindex: 101, entries: nil})
// Process the first message.
n.processAppendEntry(aeMsg1, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 0)
n.processAppendEntry(aeHeartbeat1, n.aesub)
require_Equal(t, n.pindex, 1)
require_Equal(t, n.commit, 1)
n.Applied(1)
require_Equal(t, n.applied, 1)
// Install a snapshot as normal to set up papplied.
require_NoError(t, n.InstallSnapshot(nil, false))
require_Equal(t, n.papplied, 1)
// Simulate being caught up from a leader with a snapshot.
snap := &snapshot{
lastTerm: 1,
lastIndex: 100,
peerstate: encodePeerState(n.currentPeerState()),
data: []byte("catchup"),
}
n.pterm = snap.lastTerm
n.pindex = snap.lastIndex
n.commit = snap.lastIndex
require_NoError(t, n.installSnapshot(snap))
require_Equal(t, n.papplied, 100)
require_Equal(t, n.applied, 1)
// Simulate uncommitted entries being truncated up to the above snapshot.
n.truncateWAL(1, 100)
require_Equal(t, n.papplied, 100)
// We haven't applied anything since the previous snapshot, so should error.
_, err := n.CreateSnapshotCheckpoint(false)
require_Error(t, err, errNoSnapAvailable)
// We have only applied the previous snapshot, so should still error as there's nothing (new) to snapshot.
n.Applied(100)
require_Equal(t, n.applied, 100)
_, err = n.CreateSnapshotCheckpoint(false)
require_Error(t, err, errNoSnapAvailable)
// Process a second message such that we can snapshot.
n.processAppendEntry(aeMsg2, n.aesub)
require_Equal(t, n.pindex, 101)
require_Equal(t, n.commit, 100)
n.processAppendEntry(aeHeartbeat2, n.aesub)
require_Equal(t, n.pindex, 101)
require_Equal(t, n.commit, 101)
n.Applied(101)
require_Equal(t, n.applied, 101)
// The checkpoint should function, load the previous/catchup snapshot and load above entry.
c, err := n.CreateSnapshotCheckpoint(false)
require_NoError(t, err)
buf, err := c.LoadLastSnapshot()
require_NoError(t, err)
require_True(t, bytes.Equal(buf, []byte("catchup")))
var count int
for ae, err := range c.AppendEntriesSeq() {
count++
require_NoError(t, err)
require_Equal(t, ae.pindex, 100)
}
require_Equal(t, count, 1)
}
func TestNRGInitSingleMemRaftNodeDefaults(t *testing.T) {
n, cleanup := initSingleMemRaftNode(t)
defer cleanup()
require_Equal(t, n.ID(), "esFhDys3")
require_Equal(t, len(n.Peers()), 1)
require_Equal(t, n.Peers()[0].ID, "esFhDys3")
require_Equal(t, n.ClusterSize(), 1)
require_True(t, n.Quorum())
}
func TestNRGReplayAddPeerKeepsClusterSize(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
s := c.servers[0]
defer c.shutdown()
storeDir := t.TempDir()
fcfg := FileStoreConfig{
StoreDir: storeDir,
BlockSize: defaultMediumBlockSize,
AsyncFlush: false,
srv: s,
}
scfg := StreamConfig{Name: "RAFT", Storage: FileStorage}
fs, err := newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg := &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
// Seed peer state as a 3-node group.
s.mu.RLock()
self := s.sys.shash[:idLen]
s.mu.RUnlock()
peerA, peerB := "yrzKKRBu", "cnrtt3eg"
require_NoError(t, s.bootstrapRaftNode(cfg, []string{self, peerA, peerB}, true))
n, err := s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
require_Equal(t, n.ClusterSize(), 3)
// Store a normal entry and a EntryAddPeer and commit them.
// The latter triggered the bug at restart.
esm := encodeStreamMsgAllowCompress("foo", "_INBOX.foo", nil, nil, 0, 0, true)
aeMsg1 := encode(t, &appendEntry{
leader: self,
term: 2,
commit: 0,
pterm: 0,
pindex: 0,
entries: []*Entry{newEntry(EntryNormal, esm)},
})
aeMsg2 := encode(t, &appendEntry{
leader: self,
term: 2,
commit: 2,
pterm: 2,
pindex: 1,
entries: []*Entry{newEntry(EntryAddPeer, []byte(peerA))},
})
n.processAppendEntry(aeMsg1, n.aesub)
n.processAppendEntry(aeMsg2, n.aesub)
// Restart from disk, forcing replay of the two WAL entries.
n.Stop()
n.WaitForStop()
fs.Stop()
fs, err = newFileStore(fcfg, scfg)
require_NoError(t, err)
cfg = &RaftConfig{Name: "TEST", Store: storeDir, Log: fs}
n, err = s.initRaftNode(globalAccountName, cfg, pprofLabels{})
require_NoError(t, err)
// If bug is present, cluster size is 1 instead of 3.
// EntryAddPeer would increases the initial cluster
// of size of 0 to 1. initRaftNode() would then add
// the remaining peers from peer state file, but would
// not adjust cluster size and quorum accordingly.
// This could lead to erroneously create singleton
// clusters after restart.
require_Equal(t, n.ClusterSize(), 3)
require_Equal(t, n.qn, 2)
n.Stop()
n.WaitForStop()
fs.Stop()
}