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
1194 lines
30 KiB
Go
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))
|
|
}
|