mirror of
https://github.com/hanzomsg/nats-server.git
synced 2026-10-30 20:27:34 +00:00
On restart, replaying EntryAddPeer could incorrectly leave a raft node at cluster size 1 instead of restoring the expected size and quorum from persisted state. This bug could lead to the following scenario: a node in a 3 node cluster could restart, reset set cluster size to 1. If the node did not receive any message from other nodes, it could campaign to become leader. Being in a single node cluster, it would win the election. Resulting in the original cluster splitting into two clusters (or two leaders at the same time). Specifically, if an EntryAddPeer was replayed on from the log, it would overwrite the cluster size and quorum to 1. The peer set is now restored before the log is replayed, and it is taken from the snapshot (if no snapshot is present then we fallback to peer.idx). If a log entry that changes membership is replayed, it will now update the cluster and quorum size correctly. Signed-off-by: Daniele Sciascia <[email protected]>
5307 lines
166 KiB
Go
5307 lines
166 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 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 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.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()
|
|
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: 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.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 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 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()
|
|
}
|