Files
upstream-nats-server/server/jetstream_cluster_long_test.go
upstream-mirror 709385cbaa
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 20:16:01 +05:00

1194 lines
30 KiB
Go

// Copyright 2022-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.
// This test file is skipped by default to avoid accidentally running (e.g. `go test ./server`)
//go:build !skip_js_tests && include_js_long_tests
package server
import (
"context"
crand "crypto/rand"
"errors"
"fmt"
"math/rand/v2"
"slices"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/nats-io/nats.go"
)
// TestLongKVPutWithServerRestarts overwrites values in a replicated KV bucket for a fixed amount of time.
// Also restarts a random server at fixed interval.
// The test fails if updates fail for a continuous interval of time, or if a server fails to restart and catch up.
func TestLongKVPutWithServerRestarts(t *testing.T) {
// RNG Seed
const Seed = 123456
// Number of keys in bucket
const NumKeys = 1000
// Size of (random) values
const ValueSize = 1024
// Test duration
const Duration = 3 * time.Minute
// If no updates successful for this interval, then fail the test
const MaxRetry = 5 * time.Second
// Minimum time between put operations
const UpdatesInterval = 1 * time.Millisecond
// Time between progress reports to console
const ProgressInterval = 10 * time.Second
// Time between server restarts
const ServerRestartInterval = 5 * time.Second
type Parameters struct {
numServers int
replicas int
storage nats.StorageType
}
test := func(t *testing.T, p Parameters) {
rng := rand.New(rand.NewPCG(uint64(Seed), 0))
// Create cluster
clusterName := fmt.Sprintf("C_%d-%s", p.numServers, p.storage)
cluster := createJetStreamClusterExplicit(t, clusterName, p.numServers)
defer cluster.shutdown()
// Connect to a random server but client will discover others too.
nc, js := jsClientConnect(t, cluster.randomServer())
defer nc.Close()
// Create bucket
kv, err := js.CreateKeyValue(&nats.KeyValueConfig{
Bucket: "TEST",
Replicas: p.replicas,
Storage: p.storage,
})
require_NoError(t, err)
// Initialize list of keys
keys := make([]string, NumKeys)
for i := 0; i < NumKeys; i++ {
keys[i] = fmt.Sprintf("key-%d", i)
}
// Initialize keys in bucket with an empty value
for _, key := range keys {
_, err := kv.Put(key, []byte{})
require_NoError(t, err)
}
// Track statistics
var stats = struct {
start time.Time
lastSuccessfulUpdate time.Time
updateOk uint64
updateErr uint64
restarts uint64
restartsMap map[string]int
}{
start: time.Now(),
lastSuccessfulUpdate: time.Now(),
restartsMap: make(map[string]int, p.numServers),
}
for _, server := range cluster.servers {
stats.restartsMap[server.Name()] = 0
}
// Print statistics
printProgress := func() {
t.Logf(
"[%s] %d updates %d errors, %d server restarts (%v)",
time.Since(stats.start).Round(time.Second),
stats.updateOk,
stats.updateErr,
stats.restarts,
stats.restartsMap,
)
}
// Print update on completion
defer printProgress()
// Pick a random key and update it with a random value
valueBuffer := make([]byte, ValueSize)
updateRandomKey := func() error {
key := keys[rand.IntN(NumKeys)]
_, err := crand.Read(valueBuffer)
require_NoError(t, err)
_, err = kv.Put(key, valueBuffer)
return err
}
// Set up timers and tickers
endTestTimer := time.After(Duration)
nextUpdateTicker := time.NewTicker(UpdatesInterval)
progressTicker := time.NewTicker(ProgressInterval)
restartServerTicker := time.NewTicker(ServerRestartInterval)
runLoop:
for {
select {
case <-endTestTimer:
break runLoop
case <-nextUpdateTicker.C:
err := updateRandomKey()
if err == nil {
stats.updateOk++
stats.lastSuccessfulUpdate = time.Now()
} else {
stats.updateErr++
if time.Since(stats.lastSuccessfulUpdate) > MaxRetry {
t.Fatalf("Could not successfully update for over %s", MaxRetry)
}
}
case <-restartServerTicker.C:
randomServer := cluster.servers[rng.IntN(len(cluster.servers))]
randomServer.Shutdown()
randomServer.WaitForShutdown()
restartedServer := cluster.restartServer(randomServer)
cluster.waitOnClusterReady()
cluster.waitOnServerHealthz(restartedServer)
cluster.waitOnAllCurrent()
stats.restarts++
stats.restartsMap[randomServer.Name()]++
case <-progressTicker.C:
printProgress()
}
}
}
testCases := []Parameters{
{numServers: 5, replicas: 3, storage: nats.MemoryStorage},
{numServers: 5, replicas: 3, storage: nats.FileStorage},
}
for _, testCase := range testCases {
name := fmt.Sprintf("N:%d,R:%d,%s", testCase.numServers, testCase.replicas, testCase.storage)
t.Run(
name,
func(t *testing.T) { test(t, testCase) },
)
}
}
// This is a RaftChainOfBlocks test that randomly starts and stops nodes to exercise recovery and snapshots.
func TestLongNRGChainOfBlocks(t *testing.T) {
const (
ClusterSize = 3
GroupSize = 3
ConvergenceTimeout = 30 * time.Second
Duration = 10 * time.Minute
PrintStateInterval = 3 * time.Second
)
// Create cluster
c := createJetStreamClusterExplicit(t, "Test", ClusterSize)
defer c.shutdown()
rg := c.createRaftGroup("ChainOfBlocks", GroupSize, newRaftChainStateMachine)
rg.waitOnLeader()
// Available operations
type TestOperation string
const (
StopOne TestOperation = "Stop one active node"
StopAll = "Stop all active nodes"
RestartOne = "Restart one stopped node"
RestartAll = "Restart all stopped nodes"
Snapshot = "Snapshot one active node"
Propose = "Propose a value via one active node"
ProposeLeader = "Propose a value via leader"
Pause = "Let things run undisturbed for a while"
Check = "Wait for nodes to converge"
)
// Weighted distribution of operations, one is randomly chosen from this vector in each iteration
opsWeighted := []TestOperation{
StopOne,
StopOne,
StopOne,
StopAll,
StopAll,
RestartOne,
RestartOne,
RestartOne,
RestartAll,
RestartAll,
RestartAll,
Snapshot,
Snapshot,
Propose,
Propose,
Propose,
Propose,
ProposeLeader,
ProposeLeader,
ProposeLeader,
ProposeLeader,
Pause,
Pause,
Pause,
Pause,
Check,
Check,
Check,
Check,
}
rngSeed := time.Now().UnixNano()
rng := rand.New(rand.NewPCG(uint64(rngSeed), 0))
t.Logf("Seed: %d", rngSeed)
// Chose a node from the list (and remove it)
pickRandomNode := func(nodes []stateMachine) ([]stateMachine, stateMachine) {
if len(nodes) == 0 {
// Input list is empty
return nodes, nil
}
// Pick random node
i := rng.IntN(len(nodes))
node := nodes[i]
// Move last element in its place
nodes[i] = nodes[len(nodes)-1]
// Return slice excluding last element
return nodes[:len(nodes)-1], node
}
// Create summary status string for all replicas
chainStatusString := func() string {
b := strings.Builder{}
for _, sm := range rg {
csm := sm.(*RCOBStateMachine)
running, blocksCount, blockHash := csm.getCurrentHash()
if running {
b.WriteString(
fmt.Sprintf(
" [%s (%s): %d blocks, hash=%s],",
csm.server().Name(),
csm.node().ID(),
blocksCount,
blockHash,
),
)
} else {
b.WriteString(
fmt.Sprintf(
" [%s (%s): STOPPED],",
csm.server().Name(),
csm.node().ID(),
),
)
}
}
return b.String()
}
// Track the highest number of blocks applied by any of the replicas
highestBlocksCount := uint64(0)
// Track active and stopped nodes
activeNodes := make([]stateMachine, 0, GroupSize)
stoppedNodes := make([]stateMachine, 0, GroupSize)
// Initially all nodes are active
activeNodes = append(activeNodes, rg...)
defer func() {
t.Logf("Final state: %s", chainStatusString())
}()
printStateTicker := time.NewTicker(PrintStateInterval)
testTimer := time.NewTimer(Duration)
start := time.Now()
iteration := 0
stopCooldown := 0
for {
iteration++
select {
case <-printStateTicker.C:
t.Logf(
"[%s] State: %s",
time.Since(start).Round(time.Second),
chainStatusString(),
)
case <-testTimer.C:
// Test completed
return
default:
// Continue
}
// Choose a random operation to perform in this iteration
nextOperation := opsWeighted[rng.IntN(len(opsWeighted))]
// If we're about to stop one or all nodes, do some sanity checks to ensure we don't
// spam stops which would constantly interrupt the chance of making progress.
if nextOperation == StopOne || nextOperation == StopAll {
if stopCooldown > 0 {
nextOperation = Propose
} else {
stopCooldown = 50
}
}
if stopCooldown > 0 {
stopCooldown--
}
if RCOBOptions.verbose {
t.Logf("Iteration %d: %s", iteration, nextOperation)
}
switch nextOperation {
case StopOne:
// Stop an active node (if any are left active)
var n stateMachine
activeNodes, n = pickRandomNode(activeNodes)
if n != nil {
n.stop()
stoppedNodes = append(stoppedNodes, n)
}
case StopAll:
// Stop all active nodes (if any are active)
for _, node := range activeNodes {
node.stop()
}
stoppedNodes = append(stoppedNodes, activeNodes...)
activeNodes = make([]stateMachine, 0, GroupSize)
case RestartOne:
// Restart a stopped node (if any are stopped)
var n stateMachine
stoppedNodes, n = pickRandomNode(stoppedNodes)
if n != nil {
n.restart()
activeNodes = append(activeNodes, n)
}
case RestartAll:
// Restart all stopped nodes (if any)
for _, node := range stoppedNodes {
node.restart()
}
activeNodes = append(activeNodes, stoppedNodes...)
stoppedNodes = make([]stateMachine, 0, GroupSize)
case Snapshot:
// Choose a random active node and tell it to create a snapshot
if len(activeNodes) > 0 {
n := activeNodes[rng.IntN(len(activeNodes))]
n.(*RCOBStateMachine).createSnapshot()
}
case Propose:
// Make an active node propose the next block (if any nodes are active)
if len(activeNodes) > 0 {
n := activeNodes[rng.IntN(len(activeNodes))]
n.(*RCOBStateMachine).proposeBlock()
}
case ProposeLeader:
// Make the leader propose the next block (if a leader is active)
leader := rg.leader()
if leader != nil {
leader.(*RCOBStateMachine).proposeBlock()
}
case Pause:
// Noop, let things run undisturbed for a little bit
time.Sleep(time.Duration(rng.IntN(250)) * time.Millisecond)
case Check:
// Restart any stopped node
for _, node := range stoppedNodes {
node.restart()
}
activeNodes = append(activeNodes, stoppedNodes...)
stoppedNodes = make([]stateMachine, 0, GroupSize)
// Ensure all nodes (eventually) converge
checkFor(t, ConvergenceTimeout, 250*time.Millisecond,
func() error {
referenceNode := rg[0]
// Save block count and hash of first node as reference
_, referenceBlocksCount, referenceHash := referenceNode.(*RCOBStateMachine).getCurrentHash()
// Compare each node against reference
for _, n := range rg {
sm := n.(*RCOBStateMachine)
running, blocksCount, blockHash := sm.getCurrentHash()
if !running {
return fmt.Errorf(
"node not running: %s (%s)",
sm.server().Name(),
sm.node().ID(),
)
}
// Track the highest block delivered by any node
if blocksCount > highestBlocksCount {
if RCOBOptions.verbose {
t.Logf(
"New highest blocks count: %d (%s (%s))",
blocksCount,
sm.s.Name(),
sm.n.ID(),
)
}
highestBlocksCount = blocksCount
}
// Each replica must match the reference node
if blocksCount != referenceBlocksCount {
return fmt.Errorf(
"different number of blocks %d (%s (%s) vs. %d (%s (%s))",
blocksCount,
sm.server().Name(),
sm.node().ID(),
referenceBlocksCount,
referenceNode.server().Name(),
referenceNode.node().ID(),
)
} else if blockHash != referenceHash {
return fmt.Errorf(
"different hash after %d blocks %s (%s (%s) vs. %s (%s (%s))",
blocksCount,
blockHash,
sm.server().Name(),
sm.node().ID(),
referenceHash,
referenceNode.server().Name(),
referenceNode.node().ID(),
)
}
}
// Verify consistency check was against the highest block known
if referenceBlocksCount < highestBlocksCount {
return fmt.Errorf(
"nodes converged below highest known block count: %d: %s",
highestBlocksCount,
chainStatusString(),
)
}
// All nodes reached the same state, check passed
return nil
},
)
}
}
}
func TestLongClusterWorkQueueMessagesNotSkipped(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
nc, js := jsClientConnect(t, c.randomServer())
defer nc.Close()
iterations := 500_000
stream := "s1"
subjf := "subj.>"
consumers := map[string]string{
"c1": "subj.c1",
"c2": "subj.c2.*",
"c3": "subj.c3",
}
sig := make(chan *nats.Msg, 900)
_, err := js.AddStream(&nats.StreamConfig{
Name: stream,
Storage: nats.FileStorage,
Subjects: []string{subjf},
Retention: nats.WorkQueuePolicy,
Duplicates: time.Minute * 2,
Replicas: 3,
MaxAge: time.Hour,
})
require_NoError(t, err)
for name, subjf := range consumers {
nc, js := jsClientConnect(t, c.randomServer())
defer nc.Close()
_, err = js.AddConsumer(stream, &nats.ConsumerConfig{
Name: name,
FilterSubject: subjf,
Replicas: 3,
AckPolicy: nats.AckExplicitPolicy,
DeliverPolicy: nats.DeliverAllPolicy,
})
require_NoError(t, err)
ps, err := js.PullSubscribe(subjf, "", nats.Bind(stream, name))
require_NoError(t, err)
go func() {
for {
msgs, err := ps.FetchBatch(300)
if errors.Is(err, nats.ErrTimeout) {
continue
}
if errors.Is(err, nats.ErrConnectionClosed) || errors.Is(err, nats.ErrSubscriptionClosed) {
return // ... for when the test finishes
}
require_NoError(t, err)
for msg := range msgs.Messages() {
go func() {
time.Sleep(time.Millisecond * time.Duration(rand.Int32N(100)))
require_NoError(t, msg.Ack())
sig <- msg
}()
}
}
}()
}
go func() {
hdrs := nats.Header{}
for i := 1; i <= iterations; i++ {
// Pick a random consumer to hit this time (map iteration order is
// non-deterministic, but break to do it just once).
for _, subj := range consumers {
hdrs.Set("Nats-Msg-Id", fmt.Sprintf("msg-%d", i))
if strings.HasPrefix(subj, "*") {
subj = strings.Replace(subj, "*", fmt.Sprintf("%d", i), 1)
}
_, err := js.PublishMsg(&nats.Msg{
Subject: subj,
Header: hdrs,
})
require_NoError(t, err)
break
}
}
}()
for i := 1; i <= iterations; i++ {
if i%10000 == 0 {
t.Logf("%d messages out of %d", i, iterations)
}
select {
case <-sig:
case <-time.After(time.Second * 2):
si, err := js.StreamInfo(stream)
require_NoError(t, err)
t.Logf("Stream info: %+v", si.State)
for name := range consumers {
ci, err := js.ConsumerInfo(stream, name)
require_NoError(t, err)
t.Logf("Consumer %q info: %+v, %+v", name, ci.AckFloor, ci.Delivered)
}
t.Fatalf("Didn't receive message %d", i)
}
}
}
func TestLongClusterJetStreamKeyValueSync(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
for _, s := range c.servers {
s.optsMu.Lock()
s.opts.LameDuckDuration = 15 * time.Second
s.opts.LameDuckGracePeriod = -15 * time.Second
s.optsMu.Unlock()
}
s := c.randomNonLeader()
connect := func(t *testing.T) (*nats.Conn, nats.JetStreamContext) {
return jsClientConnect(t, s)
}
const accountName = "$G"
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
createData := func(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.IntN(len(letterBytes))]
}
return b
}
getOrCreateKvStore := func(kvname string) (nats.KeyValue, error) {
_, js := connect(t)
kvExists := false
existingKvnames := js.KeyValueStoreNames()
for existingKvname := range existingKvnames {
if existingKvname == kvname {
kvExists = true
break
}
}
if !kvExists {
return js.CreateKeyValue(&nats.KeyValueConfig{
Bucket: kvname,
Replicas: 3,
Storage: nats.FileStorage,
})
} else {
return js.KeyValue(kvname)
}
}
abs := func(x int64) int64 {
if x < 0 {
return -x
}
return x
}
var counter int64
var errorCounter int64
checkMsgsEqual := func(t *testing.T, accountName, streamName string) error {
// Gather all the streams replicas and compare contents.
msets := make(map[*Server]*stream)
for _, s := range c.servers {
acc, err := s.LookupAccount(accountName)
if err != nil {
return err
}
mset, err := acc.lookupStream(streamName)
if err != nil {
return err
}
msets[s] = mset
}
str := getStreamDetails(t, c, accountName, streamName)
if str == nil {
return fmt.Errorf("could not get stream leader state")
}
state := str.State
for seq := state.FirstSeq; seq <= state.LastSeq; seq++ {
var msgId string
var smv StoreMsg
for replica, mset := range msets {
mset.mu.RLock()
sm, err := mset.store.LoadMsg(seq, &smv)
mset.mu.RUnlock()
if err != nil {
if err == ErrStoreMsgNotFound || err == errDeletedMsg {
// Skip these.
} else {
t.Logf("WRN: Error loading message (seq=%d) from stream %q on replica %q: %v", seq, streamName, replica, err)
}
continue
}
if msgId == _EMPTY_ {
msgId = string(sm.hdr)
} else if msgId != string(sm.hdr) {
t.Errorf("MsgIds do not match for seq %d on stream %q: %q vs %q", seq, streamName, msgId, sm.hdr)
}
}
}
return nil
}
keyUpdater := func(ctx context.Context, cancel context.CancelFunc, kvname string, numKeys int) {
kv, err := getOrCreateKvStore(kvname)
if err != nil {
t.Fatalf("[%s]:%v", kvname, err)
}
for i := 0; i < numKeys; i++ {
key := fmt.Sprintf("key-%d", i)
kv.Create(key, createData(160))
}
lastData := make(map[string][]byte)
revisions := make(map[string]uint64)
for {
select {
case <-ctx.Done():
return
default:
}
r := rand.IntN(numKeys)
key := fmt.Sprintf("key-%d", r)
for i := 0; i < 5; i++ {
_, err := kv.Get(key)
if err != nil {
atomic.AddInt64(&errorCounter, 1)
if err == nats.ErrKeyNotFound {
t.Logf("WRN: Key not found! [%s/%s] - [%s]", kvname, key, err)
cancel()
}
}
}
k, err := kv.Get(key)
if err != nil {
atomic.AddInt64(&errorCounter, 1)
} else {
if revisions[key] != 0 && abs(int64(k.Revision())-int64(revisions[key])) < 2 {
lastDataVal, ok := lastData[key]
if ok && k.Revision() == revisions[key] && slices.Compare(lastDataVal, k.Value()) != 0 {
t.Logf("data loss [%s/%s][rev:%d] expected:[%v] is:[%v]", kvname, key, revisions[key], string(lastDataVal), string(k.Value()))
}
}
newData := createData(160)
revisions[key], err = kv.Update(key, newData, k.Revision())
if err != nil && err != nats.ErrTimeout {
atomic.AddInt64(&errorCounter, 1)
} else {
lastData[key] = newData
}
atomic.AddInt64(&counter, 1)
}
}
}
streamCount := 50
keysCount := 100
streamPrefix := "IKV"
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// The keyUpdaters will run for less time.
kctx, kcancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer kcancel()
var wg sync.WaitGroup
var streams []string
for i := 0; i < streamCount; i++ {
streamName := fmt.Sprintf("%s-%d", streamPrefix, i)
streams = append(streams, "KV_"+streamName)
wg.Add(1)
go func(i int) {
defer wg.Done()
keyUpdater(kctx, cancel, streamName, keysCount)
}(i)
}
debug := false
nc2, _ := jsClientConnect(t, s)
if debug {
go func() {
for range time.NewTicker(5 * time.Second).C {
select {
case <-ctx.Done():
return
default:
}
for _, str := range streams {
leaderSrv := c.streamLeader(accountName, str)
if leaderSrv == nil {
continue
}
streamLeader := getStreamDetails(t, c, accountName, str)
if streamLeader == nil {
continue
}
t.Logf("|------------------------------------------------------------------------------------------------------------------------|")
lstate := streamLeader.State
t.Logf("| %-10s | %-10s | msgs:%-10d | bytes:%-10d | deleted:%-10d | first:%-10d | last:%-10d |",
str, leaderSrv.String()+"*", lstate.Msgs, lstate.Bytes, lstate.NumDeleted, lstate.FirstSeq, lstate.LastSeq,
)
for _, srv := range c.servers {
if srv == leaderSrv {
continue
}
acc, err := srv.LookupAccount(accountName)
if err != nil {
continue
}
stream, err := acc.lookupStream(str)
if err != nil {
t.Logf("Error looking up stream %s on %s replica", str, srv)
continue
}
state := stream.state()
unsynced := lstate.Msgs != state.Msgs || lstate.Bytes != state.Bytes ||
lstate.NumDeleted != state.NumDeleted || lstate.FirstSeq != state.FirstSeq || lstate.LastSeq != state.LastSeq
var result string
if unsynced {
result = "UNSYNCED"
}
t.Logf("| %-10s | %-10s | msgs:%-10d | bytes:%-10d | deleted:%-10d | first:%-10d | last:%-10d | %s",
str, srv, state.Msgs, state.Bytes, state.NumDeleted, state.FirstSeq, state.LastSeq, result,
)
}
}
t.Logf("|------------------------------------------------------------------------------------------------------------------------| %v", nc2.ConnectedUrl())
}
}()
}
checkStreams := func(t *testing.T) {
for _, str := range streams {
checkFor(t, time.Minute, 500*time.Millisecond, func() error {
return checkState(t, c, accountName, str)
})
checkFor(t, time.Minute, 500*time.Millisecond, func() error {
return checkMsgsEqual(t, accountName, str)
})
}
}
Loop:
for range time.NewTicker(30 * time.Second).C {
select {
case <-ctx.Done():
break Loop
default:
}
rollout := func(t *testing.T) {
for _, s := range c.servers {
// For graceful mode
s.lameDuckMode()
s.WaitForShutdown()
s = c.restartServer(s)
hctx, hcancel := context.WithTimeout(context.Background(), 15*time.Second)
defer hcancel()
Healthz:
for range time.NewTicker(2 * time.Second).C {
select {
case <-hctx.Done():
default:
}
status := s.healthz(nil)
if status.StatusCode == 200 {
break Healthz
}
}
c.waitOnClusterReady()
checkStreams(t)
}
}
rollout(t)
checkStreams(t)
}
wg.Wait()
checkStreams(t)
}
func TestLongClusterCLFSOnDuplicates(t *testing.T) {
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
nc, js := jsClientConnect(t, c.randomServer())
defer nc.Close()
nc2, js2 := jsClientConnect(t, c.randomServer())
defer nc2.Close()
streamName := "TESTW"
_, err := js.AddStream(&nats.StreamConfig{
Name: streamName,
Subjects: []string{"foo"},
Replicas: 3,
Storage: nats.FileStorage,
MaxAge: 3 * time.Minute,
Duplicates: 2 * time.Minute,
})
require_NoError(t, err)
c.waitOnStreamLeader(globalAccountName, streamName)
var wg sync.WaitGroup
// The test will be successful if it runs for this long without dup issues.
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
go func() {
tick := time.NewTicker(10 * time.Second)
for {
select {
case <-ctx.Done():
wg.Done()
return
case <-tick.C:
c.streamLeader(globalAccountName, streamName).JetStreamStepdownStream(globalAccountName, streamName)
}
}
}()
wg.Add(1)
for i := 0; i < 5; i++ {
go func(i int) {
var err error
sub, err := js2.PullSubscribe("foo", fmt.Sprintf("A:%d", i))
require_NoError(t, err)
for {
select {
case <-ctx.Done():
wg.Done()
return
default:
}
msgs, err := sub.Fetch(100, nats.MaxWait(200*time.Millisecond))
if err != nil {
continue
}
for _, msg := range msgs {
msg.Ack()
}
}
}(i)
wg.Add(1)
}
// Sync producer that only does a couple of duplicates, cancel the test
// if we get too many errors without responses.
errCh := make(chan error, 10)
go func() {
// Try sync publishes normally in this state and see if it times out.
for i := 0; ; i++ {
select {
case <-ctx.Done():
wg.Done()
return
default:
}
var succeeded bool
var failures int
for n := 0; n < 10; n++ {
_, err := js.Publish(
"foo", []byte("test"),
nats.MsgId(fmt.Sprintf("sync:checking:%d", i)),
nats.RetryAttempts(30),
nats.AckWait(500*time.Millisecond),
)
if err != nil {
failures++
continue
}
succeeded = true
}
if !succeeded {
errCh <- fmt.Errorf("Too many publishes failed with timeout: failures=%d, i=%d", failures, i)
}
}
}()
wg.Add(1)
Loop:
for n := uint64(0); true; n++ {
select {
case <-ctx.Done():
break Loop
case e := <-errCh:
t.Error(e)
break Loop
default:
}
// Cause a lot of duplicates very fast until producer stalls.
for i := 0; i < 128; i++ {
msgID := nats.MsgId(fmt.Sprintf("id.%d.%d", n, i))
js.PublishAsync("foo", []byte("test"), msgID, nats.RetryAttempts(10))
}
}
cancel()
wg.Wait()
}
func TestLongClusterJetStreamRestartThenScaleStreamReplicas(t *testing.T) {
t.Skip("This test takes too long, need to make shorter")
c := createJetStreamClusterExplicit(t, "R3S", 3)
defer c.shutdown()
s := c.randomNonLeader()
nc, js := jsClientConnect(t, s)
defer nc.Close()
nc2, producer := jsClientConnect(t, s)
defer nc2.Close()
_, err := js.AddStream(&nats.StreamConfig{
Name: "TEST",
Subjects: []string{"foo"},
Replicas: 3,
})
require_NoError(t, err)
c.waitOnStreamLeader(globalAccountName, "TEST")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
end := time.Now().Add(2 * time.Second)
for time.Now().Before(end) {
producer.Publish("foo", []byte(strings.Repeat("A", 128)))
time.Sleep(time.Millisecond)
}
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
sub, err := js.PullSubscribe("foo", fmt.Sprintf("C-%d", i))
require_NoError(t, err)
wg.Add(1)
go func() {
defer wg.Done()
for range time.NewTicker(10 * time.Millisecond).C {
select {
case <-ctx.Done():
return
default:
}
msgs, err := sub.Fetch(1)
if err != nil && !errors.Is(err, nats.ErrTimeout) && !errors.Is(err, nats.ErrConnectionClosed) {
t.Logf("Pull Error: %v", err)
}
for _, msg := range msgs {
msg.Ack()
}
}
}()
}
c.lameDuckRestartAll()
c.waitOnStreamLeader(globalAccountName, "TEST")
// Swap the logger to try to detect the condition after the restart.
loggers := make([]*captureDebugLogger, 3)
for i, srv := range c.servers {
l := &captureDebugLogger{dbgCh: make(chan string, 10)}
loggers[i] = l
srv.SetLogger(l, true, false)
}
condition := `Direct proposal ignored, not leader (state: CLOSED)`
errCh := make(chan error, 10)
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case dl := <-loggers[0].dbgCh:
if strings.Contains(dl, condition) {
errCh <- errors.New(condition)
}
case dl := <-loggers[1].dbgCh:
if strings.Contains(dl, condition) {
errCh <- errors.New(condition)
}
case dl := <-loggers[2].dbgCh:
if strings.Contains(dl, condition) {
errCh <- errors.New(condition)
}
case <-ctx.Done():
return
}
}
}()
// Start publishing again for a while.
end = time.Now().Add(2 * time.Second)
for time.Now().Before(end) {
producer.Publish("foo", []byte(strings.Repeat("A", 128)))
time.Sleep(time.Millisecond)
}
// Try to do a stream edit back to R=1 after doing all the upgrade.
info, _ := js.StreamInfo("TEST")
sconfig := info.Config
sconfig.Replicas = 1
_, err = js.UpdateStream(&sconfig)
require_NoError(t, err)
// Leave running for some time after the update.
time.Sleep(2 * time.Second)
info, _ = js.StreamInfo("TEST")
sconfig = info.Config
sconfig.Replicas = 3
_, err = js.UpdateStream(&sconfig)
require_NoError(t, err)
select {
case e := <-errCh:
t.Fatalf("Bad condition on raft node: %v", e)
case <-time.After(2 * time.Second):
// Done
}
// Stop goroutines and wait for them to exit.
cancel()
wg.Wait()
}
func TestLongFileStoreEnforceMsgPerSubjectLimit(t *testing.T) {
td := t.TempDir()
fs, err := newFileStore(
FileStoreConfig{StoreDir: td, BlockSize: 1024},
StreamConfig{
Name: "zzz", Subjects: []string{"test.>"}, Storage: FileStorage,
},
)
require_NoError(t, err)
defer fs.Stop()
t.Logf("Starting publishes")
for i := 0; i < 100_000; i++ {
_, _, err := fs.StoreMsg(fmt.Sprintf("test.%06d", i), nil, nil, 0)
require_NoError(t, err)
}
// Now update some of them. Leave a bit of a mess with some big gaps.
for i := 0; i < 5_000_000; i++ {
n := rand.Int32N(100_000)
if n < 5000 {
continue
}
_, _, err := fs.StoreMsg(fmt.Sprintf("test.%06d", n), nil, nil, 0)
require_NoError(t, err)
}
t.Logf("Publish complete")
require_NoError(t, fs.Stop())
fs, err = newFileStore(
FileStoreConfig{StoreDir: td, BlockSize: 1024},
StreamConfig{
Name: "zzz", Subjects: []string{"test.>"}, Storage: FileStorage,
MaxMsgsPer: 1,
},
)
require_NoError(t, err)
defer fs.Stop()
// Mangle the filestore state and then see how long it takes to enforce
// the per-subject limit.
fs.state.Msgs++
start := time.Now()
fs.enforceMsgPerSubjectLimit(false)
require_LessThan(t, time.Since(start), time.Minute)
t.Logf("Took %s", time.Since(start))
}